1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

Enter キーでプロンプトを送信するのがどうしても我慢できない。

チャット UI でも AI CLI でも、Enter は改行であるべきだ。
送信は Ctrl+Enter。そう決めている。コードを書いていて思考の途中に誤って送信してしまうストレスを、もう味わいたくない。

この「Enter = 改行、Ctrl+Enter = 送信」をあらゆる環境で統一することが今回の目標だった。
Claude Code(claude)と OpenAI Codex(codex)をtmux, neovim+sidekickから使い倒す中で、Mac+SSH+tmux 環境の codex だけ Ctrl+Enter がまったく効かないという壁にぶつかった。

調査と試行錯誤の記録を残しておく。

調査の90%はClaude CodeでSonnet 4.6が実施。この記事の90%もClaude Sonnet4.6が書いています。


TL;DR

  • Mac+SHH+tmux環境下、Claude(Node.js)は動く、Codex(Rust/crossterm)だけCtrl+Enter送信が動かないという非対称な現象
  • 原因は SSH レイテンシによる kitty keyboard protocol ネゴシエーションのタイムアウト
  • 解決策は「Codex が kitty mode なしで認識できるキー(Alt+Enter)を使う」4層構成

環境

項目 内容
ターミナル Ghostty
マルチプレクサ tmux
エディタ Neovim + sidekick.nvim
AI CLI Claude Code(claude)/ OpenAI Codex(codex
接続形態 Mac → SSH → リモート Mac mini サーバ

sidekick.nvim は Neovim 内から AI CLI をサイドペインに起動するプラグイン。tmux バックエンドで pane を管理する。


現象

Neovim の sidekick.nvim 経由で claudecodex を起動したとき:

  • claude → Ctrl+Enter でプロンプト送信 ✅
  • codex → Ctrl+Enter が改行として機能

同様のコンフィグでも 別環境Windows+WSL+Neovim+sidekick では codex の Ctrl+Enter が動く。

Ghostty で直接 tmux + codex を起動(Neovim 経由なし)しても Ctrl+Enter は効かない。


原因を掘り下げる

Ctrl+Enter は「普通の文字」ではない

ターミナルに Ctrl+Enter を送るとき、どんなバイト列になるかはターミナルの実装による。

レガシーモード(旧来の VT エスケープ):
Ctrl+Enter は特別なシーケンスを持たない。\r(キャリッジリターン = 普通の Enter)として届くことが多い。

kitty keyboard protocol(拡張モード):

\033[13;5u   ← CSI-u シーケンス。13 = Enter、5 = Ctrl modifier

このモードでは Enter・Ctrl+Enter・Shift+Enter がすべて区別できる。

crossterm と kitty keyboard protocol

Codex の TUI は Rust の crossterm ライブラリを使っている。crossterm が Ctrl+Enter を認識するためには、起動時にターミナルに対して「kitty keyboard protocol を使う」というネゴシエーションを完了する必要がある。

具体的には、crossterm がエスケープシーケンスをターミナルに送り、ターミナルが応答するまで待つ。

SSH レイテンシがネゴシエーションを殺す

ここが核心。

crossterm → エスケープシーケンスを送信
  ↓ SSH の RTT がかかる
ターミナル(Ghostty)が応答
  ↓ SSH の RTT がかかる
crossterm が受信

ローカル(WSL など) では RTT がほぼゼロ → ネゴシエーション成功 → kitty mode ON → \033[13;5u を Ctrl+Enter と認識 ✅

SSH 越し では RTT が数十〜数百 ms → crossterm のクエリがタイムアウト → kitty mode に入れない → \033[13;5u は未知のシーケンスとして無視される

なぜ Claude は動くのか

claude CLI は Node.js 実装。Claude は CSI-u シーケンスをネゴシエーションなしで直接バイト列としてパースする。そのため SSH 越しでも \033[13;5u を受け取れば Ctrl+Enter と認識できる。

なぜ Windows Terminal では動くのか

Windows Terminal には sendInput というキーバインドアクションがあり、物理キー入力をバイパスして任意のバイト列を直接注入できる:

{
  "command": {
    "action": "sendInput",
    "input": "[13;5u"
  }
}

Ctrl+Enter を押すと Windows Terminal が \033[13;5u を強制送信すル設定を追加してあった。
WSL はローカル接続なので kitty mode ネゴシエーションが成功しており、crossterm がこのバイト列を正しく認識できる。


解決策

kitty mode ネゴシエーションを SSH 越しで修正するのは困難なため、方針を転換:

「Codex が kitty mode なしで認識できるキーを使う」

そのキーが Alt+Enter\033\r = ESC + CR)。レガシーモードで確立された古典的なエンコーディングで、ネゴシエーション不要。

ただしそのまま alt-enter を Codex の config に追加すると:

Error: invalid `tui.keymap`

エラーの原因:デフォルトバインドとの競合

Codex の alt-enter はデフォルトで editor.insert_newline(改行挿入)にバインドされている。同じキーを submit にも割り当てようとするとコンフリクトで設定全体が無効になる。

修正:insert_newline から alt-enter を明示的に除外してから submit に追加。

# ~/.codex/config.toml
[tui.keymap.editor]
insert_newline = ["enter", "ctrl-j", "ctrl-m", "shift-enter"]  # alt-enter を除去

[tui.keymap.composer]
submit = ["ctrl-enter", "alt-enter"]  # alt-enter を追加

実装:4層の変更

1. Ghostty(最外層)

Windows Terminal の sendInput と同じ設定を Ghostty にも追加。text: アクションは \x 記法でバイト列を直接送出できる:

# ghostty/config
keybind = ctrl+enter=text:\x1b[13;5u

これで Ctrl+Enter を押したとき Ghostty が \033[13;5u を強制送信する。

2. tmux(中間層)

tmux が \033[13;5u を受け取ったとき、アクティブな pane のコマンドによって送り先を変える:

# tmux.conf
set -g extended-keys on  # "always" ではなく "on"
bind-key -n C-Enter run-shell "~/.config/tmux/ctrl-enter.sh '#{pane_id}' '#{pane_current_command}'"
#!/bin/bash
# ~/.config/tmux/ctrl-enter.sh
pane_id="$1"
pane_cmd="$2"

case "$pane_cmd" in
    codex)
        if [[ -f /proc/sys/fs/binfmt_misc/WSLInterop ]]; then
            # WSL: Codex は kitty mode で動作 → CSI-u をそのまま送る
            tmux send-keys -t "$pane_id" -l $'\x1b[13;5u'
        else
            # Mac+SSH: kitty mode なし → Alt+Enter で submit
            tmux send-keys -t "$pane_id" -l $'\x1b\r'
        fi
        ;;
    nvim|vim)
        # Neovim には CSI-u をそのまま渡す(Neovim 側でマッピングを処理)
        tmux send-keys -t "$pane_id" -l $'\x1b[13;5u'
        ;;
    *)
        # Claude 等:CSI-u をそのまま
        tmux send-keys -t "$pane_id" -l $'\x1b[13;5u'
        ;;
esac

ポイント:send-keys -l フラグでキー名解釈をスキップし、生バイトを直接 pane の stdin に注入する。
なお、検討の途中で -l なしで send-keys C-Enter を使い、tmux が独自解釈した別のシーケンスを送る事象が発生した。

3. Neovim / sidekick.lua(Sidekick 経由の場合)

sidekick.nvim が Codex の job に送るバイト列を、SSH_TTY 環境変数で切り替える:

local is_ssh = vim.fn.getenv("SSH_TTY") ~= vim.NIL

vim.keymap.set("t", "<C-CR>", function()
	local job = vim.b[args.buf].terminal_job_id
	if job then
		local seq
		if tool.name == "codex" and is_ssh then
			seq = "\027\r" -- Alt+Enter(SSH = kitty mode なし)
		else
			seq = "\027[13;5u" -- Ctrl+Enter CSI-u(kitty mode あり)
		end
		vim.api.nvim_chan_send(job, seq)
	end
end, { buffer = args.buf })

SSH_TTY は SSH セッション中のみ設定される環境変数。WSL のローカル接続では未設定なので、Windows+WSL の既存動作を壊さない。

4. Codex config(keymap 設定)

前述の TOML 設定を適用。


動作マトリクス

環境 経路 送信バイト 認識
Mac+SSH tmux → Codex 直接 \x1b\r(Alt+Enter)
Mac+SSH Neovim → sidekick → Codex \x1b\r(SSH 検出)
Mac+SSH tmux → Claude 直接 \x1b[13;5u(CSI-u)
Mac+SSH Neovim → sidekick → Claude \x1b[13;5u
Windows+WSL Neovim → sidekick → Codex \x1b[13;5u(kitty mode)
Windows+WSL tmux → Codex 直接 \x1b[13;5u(WSLInterop 検出)

まとめ

レイヤー 変更内容 目的
Ghostty ctrl+enter=text:\x1b[13;5u バイト列の強制注入(Windows Terminal 方式)
Codex config alt-enter を submit に追加、insert_newline から除去 コンフリクト解消
tmux スクリプト pane コマンドで送信先を分岐 Codex には Alt+Enter、他には CSI-u
sidekick.lua SSH_TTY で SSH 検出 Windows+WSL の既存動作を保護

SSH 環境での TUI ツールのキー入力は、ターミナルエミュレータ・SSH・tmux・アプリ(TUI ライブラリ)の4層がそれぞれのプロトコルを持っており、どこか一箇所でも食い違うと失敗する。今回の調査で得た最大の教訓は「なぜ同じ設定が別の環境で動くのか、動作原理を追う」ことの重要性だった。

とは、この検討に1時間を費やしたClaude Sonnet 4.6さんの弁

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?