Blog

あらためてAgentとAgentをtmuxで会話させる方法

image

前置き

Claude Code で Claude Fable が使えるようになったことで、
改めてエージェント同士で会話させながらオペレーションを進めるということが重要になってきました。

具体的には、Claude Fable が結構トークンを消費して、
Claude の使用量がカツカツになる場合が多いので、
僕の場合は Claude Code と Codex を併用しながら動かしています。

そこで、Claude Code と Codex を行き来する方法として、
tmux が非常に優秀だなと改めて見直しました。
今回は、tmux の便利な使い方について解説していこうと思います。

こまっていること

  • Claude Code から指示を出して Codex(ないし任意のエージェント) に作業をさせたい
  • いちいち CC の指示をコピペして Codex に持って行くは今更やりたくない
  • サブエージェントもいいのだけれども、Claude の Usage を使いたくない

やること

  • tmux がわからなくてもスキルを使うことで簡単にセットアップできる
  • tmux の基本的なコンフィグと使い方を学ぶことで最低限の動きができる
  • tmux を使うことで Claude Code と Codex、その他 CLI との連携がやりやすくなる

前提

下記が使える

  • Claude Code
  • Codex
  • Mac
  • Homebrew
  • 任意のターミナル

Tmux について

Tmux って何よ

とほほのtmux入門 より引用。

  • ターミナルマルチプレクサ (Terminal Multiplexer) の略です
  • Linux 系のターミナル画面を複数のセッション、ウィンドウ、ペインに分割して利用することができます
  • ひとつのターミナルは複数のセッションを持つことができます
  • ひとつのセッションは複数のウィンドウを持つことができます
  • ひとつのウィンドウは複数のペインを持つことができます
  • ターミナルを終了してもセッションは維持されます。コマンドを実行して翌日の朝結果を確認しようとしたら SSH ログアウトしてしまっていたという悲劇を避けることができます。

つまりは、1 つのターミナルの中で複数のウィンドウを分割し、
そのウィンドウ同士で会話ができるプラットフォームということです。

詳しくは下記サイトなどを見てもらえればと思います。
とほほのtmux入門 - とほほのWWW入門

どうつかうの

下記コマンドでインストールします。
他の方法もありますが基本僕はこれで入れます。

brew install tmux

インストールしたら、tmux コマンドで起動できます。

image

初期設定

ただ個人的に、初期の設定のままだとだいぶ使いづらいと感じるので、
とりあえず設定しておくと便利で使いやすくなる項目をいくつか入れておきます。

~/.tmux.conf があるので、これを触ります。
なかったら作ってください。

# プレフィックスキーをC-aに変更(デフォルトのC-bより押しやすい)
unbind C-b
set -g prefix C-a
bind C-a send-prefix

# マウス操作を有効化
set -g mouse on

# ウィンドウ番号を1から開始(デフォルトは0)
set -g base-index 1

# ペイン番号を1から開始
setw -g pane-base-index 1

# 設定ファイルのリロード
bind r source-file ~/.tmux.conf \; display "設定ファイルをリロードしました!"

# OSCエスケープシーケンス無効化(VSCode統合端末対策)
set -g allow-passthrough off


# ペイン分割のキーバインド改善
bind \\ split-window -h # 縦分割
bind - split-window -v # 横分割

# OSCシーケンス完全ブロック
set -g set-titles off
set -g set-titles-string ""
bind-key w run-shell "
tmux list-windows -a -F '#S:#I:#W' |
fzf |
awk -F: '{print \$1\":\"\$2}' |
xargs -I{} tmux select-window -t {}
"

これで

  • ctrl-a でいろんな事ができるようになる(初期設定は ctrl-b で若干押しづらい)
  • マウスで操作できるようになる
  • ペインが -| で切れるようになる
  • ペインが 1 から始まって直感的になる
  • 設定をいじるときは ctrl-a を押してから r で設定ファイルをリロードできる
  • vscode で変にならなくなる

っていう感じに設定ができました。

詳しくは とほほのtmux入門 のカスタマイズ項目 を見てください。

基本的に人間が使うにおいて tmux で覚えることは、「Ctrl-CCtrl-D で、ターミナルを終了したり止めたりする」で十分と考えます。
あとは適宜、人間が使うための設定を入れておくと便利です。

文章のコピーが必要になった場合は、下記設定を入れた上でドラッグして Enter を押せば、そのドラッグした部分がコピーされます。
詳しくは「コピーモード」で調べてください。

# コピーモード設定
# viキーバインドを使用
setw -g mode-keys vi

# コピーモードのキーバインド
bind-key -T copy-mode-vi 'v' send -X begin-selection     # v で選択開始
bind-key -T copy-mode-vi 'y' send -X copy-pipe-and-cancel 'pbcopy'  # y でコピー(macOSクリップボード連携)
bind-key -T copy-mode-vi Enter send -X copy-pipe-and-cancel 'pbcopy'  # Enter でもコピー
bind-key -T copy-mode-vi 'r' send -X rectangle-toggle    # r で矩形選択

# コピーモードの見た目設定
set -g mode-style 'fg=colour196 bg=colour238 bold'

# コピーモードの改善
bind c copy-mode
bind v paste-buffer

基本的にここからは、tmux をエージェントに使わせる前提で進めていきます。

エージェントと Tmux を連携する

準備

では実際に tmux を使って、エージェント同士を会話させていきます。

エージェントが会話するために必要なのは、指示役のエージェントと実行役のエージェントに役割を分けることです。これを tmux のペインを分ける機能によって実現します。

tmux 同士でコマンドを送り合うことができるようになり、エージェント同士の会話が成立します。まずはペインを分けるところから始めましょう。

とはいっても、実際は ctrl-a-or| で切って、そのペインで claude ないし codex を実行すれば終わりです。

実際に動かす

tmux をエージェントに認識させることにおいて重要なのは

  • tmux を使っていること、
  • あなたの役割は何なのか
  • 別の役割に指示を送るには、どうしたらいいのか
    ということを認識させることです。

これを指示するには、次のようなプロンプトを使います。

tmuxを使って、あなたは指示役です。

現在ペイン2で実行役が待っているので、あなたはペイン2に対して指示を送ってください。指示を送ったらエンターキーで送信し、必ず指示が実行されるようにしてください。

あなたは指示役なので、基本的にコードの実装は行わないでください。

毎回これを入力するのはとても大変なことなので、
僕は下記のようなスキルを作って運用しています。

tmux-agent-bridge

あとは tmux の知識は基本的にエージェントが持っているので、
特に特別なことをするでもなく、
tmux を使って勝手にペイン同士が会話して動かしてくれます。

おわりに

これで簡単に tmux を使って、エージェント同士を会話させながらエージェントコーディングをすることができます。

だいぶ快適にエージェント同士を会話させて、しばらく動いてくれるので、人間がすることはあとの監視と、余った時間で別のことをするということだけになります。

というわけで、tmux を使った快適なエージェントコーディングライフを楽しんでください。

Tips: 毎回準備するのがめんどい場合のカスタマイズ

僕はこれを毎回準備したり、あとはペインが増えてきて開発サーバーを動かしたいときなどがよくあるので、開発のたびにこれらを準備するのはとても面倒です。

そのため、zshrc にコマンドを仕込んで、以下の作業を自動で行えるようにしています:

  1. ワークツリーを自動で切る
  2. tmux を自動で分割する
  3. 開発サーバー、Claude、Codex をそれぞれ起動する

これの説明は省略するので、とりあえず、下記のような内容を .zshrc にぶち込んでいるということだけ分かればいいです。


# 現在のリポジトリで新しい worktree を作って tmux 4 分割 IDE を起動する
# usage: wtptmux [slug]
#   - slug 省略時は sotono/YYYYMMDDHHMMSS のブランチを作成
#   - slug 指定時は sotono/<slug> のブランチを作成
#   - fzf でベースブランチ(ローカル + リモート、更新順)を選択
wtptmux() {
    if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
        echo "このディレクトリはGitリポジトリではありません"
        return 1
    fi

    local slug="${1:-$(date +%Y%m%d%H%M%S)}"
    local new_branch="sotono/${slug}"

    if git show-ref --verify --quiet "refs/heads/${new_branch}"; then
        echo "ブランチが既に存在します: ${new_branch}"
        return 1
    fi

    local base_choice
    base_choice=$({
        git for-each-ref --sort=-committerdate refs/heads/ \
            --format='%(committerdate:relative)%09%(refname:short)%09%(refname:short)'
        git for-each-ref --sort=-committerdate refs/remotes/ \
            --format='%(committerdate:relative)%09%(refname:short)%09%(refname:short)' |
            grep -v '/HEAD$'
    } | awk -F '\t' '!seen[$2]++' |
        fzf --prompt='base branch> ' \
            --header='ベースブランチを選択 (更新順)' \
            --with-nth=1,2 --delimiter=$'\t')

    if [[ -z "$base_choice" ]]; then
        echo "キャンセルしました"
        return 1
    fi

    local base_branch
    base_branch=$(printf '%s\n' "$base_choice" | cut -f3-)

    local base_commit="$base_branch"
    if [[ "$base_branch" == */* ]] && git show-ref --verify --quiet "refs/remotes/${base_branch}"; then
        base_commit="$base_branch"
    fi

    echo "Creating worktree: ${new_branch} (from ${base_branch})"
    if ! wtp add -b "${new_branch}" "${base_commit}"; then
        echo "wtp add に失敗しました"
        return 1
    fi

    local worktree_path
    worktree_path=$(wtp cd "${new_branch}") || {
        echo "wtp cd でパスを取得できませんでした: ${new_branch}"
        return 1
    }

    cd "${worktree_path}" || return 1
    echo "${worktree_path}" >> "$REPOHIST_FILE"

    _launch_tmux_ide_or_init "${worktree_path}"
}

# tmux session 名として安全な短い worktree 識別子を作る内部関数
_tmux_worktree_session_name() {
    local repo_path="$1"
    local real_path
    real_path=$(cd "$repo_path" 2>/dev/null && pwd -P) || return 1

    local project_name
    project_name=$(basename "$real_path")
    local safe_project="${project_name//[^A-Za-z0-9_-]/-}"
    local path_hash
    path_hash=$(printf '%s' "$real_path" | cksum | awk '{print $1}')

    printf 'ide-%s-%s\n' "$safe_project" "$path_hash"
}

# worktree ごとに tmux の 4 分割 IDE session を作成し、既存ならそこへ移動する内部関数
_launch_tmux_ide_or_init() {
    local repo_path="$1"
    if [[ -z "$repo_path" || ! -d "$repo_path" ]]; then
        echo "worktree path が見つかりません: $repo_path"
        return 1
    fi

    local session_name
    session_name=$(_tmux_worktree_session_name "$repo_path") || return 1

    if ! tmux has-session -t "$session_name" 2>/dev/null; then
        local dev_command='zsh -lc '\''if [[ -f package.json ]] && command -v bun >/dev/null 2>&1 && grep -q "\"dev:all\"" package.json; then bun run dev:all; else exec zsh; fi'\'''
        local claude_pane codex_pane shell_pane dev_pane

        tmux new-session -d -s "$session_name" -n main -c "$repo_path" "claude --model fable" || return 1
        claude_pane=$(tmux display-message -p -t "$session_name:0.0" '#{pane_id}') || return 1
        tmux select-pane -t "$claude_pane" -T "Claude"

        codex_pane=$(tmux split-window -h -P -F '#{pane_id}' -t "$claude_pane" -c "$repo_path" "codex") || return 1
        tmux select-pane -t "$codex_pane" -T "Codex"

        shell_pane=$(tmux split-window -v -P -F '#{pane_id}' -t "$claude_pane" -c "$repo_path") || return 1
        tmux select-pane -t "$shell_pane" -T "Shell"

        dev_pane=$(tmux split-window -v -P -F '#{pane_id}' -t "$codex_pane" -c "$repo_path" "$dev_command") || return 1
        tmux select-pane -t "$dev_pane" -T "Dev Server"

        tmux select-layout -t "$session_name:0" tiled >/dev/null
        tmux select-pane -t "$claude_pane"
    fi

    if [[ -n "${TMUX:-}" ]]; then
        tmux switch-client -t "$session_name"
    else
        tmux attach-session -t "$session_name"
    fi
}
SNS
Follow Me