0. 概要
私は殆どの場合、sshした先で作業するのですが、ssh先は商用環境やそのステージング環境なので、codexコマンドやclaudeコマンドをインストールすることができません。
これまでは、構築や調査でAIの支援を受けるときは、TeraTermのログを食わせていたのですが、やはり操作自体任せてみたいと思い、今回のような仕組みを構築してみました。
AIが何をしでかすか分からないので、使うのは構築時のみにしてください。
やってみた印象ですが、Claude Codeは事前に実行コマンドを教えてくれるので安心ですが、Codexは勝手に色々するので怖かったです。
1. 指針と効能
- 人手で作業対象にsshしておく
- sshした環境はscreenセッションとして再利用可能にしておく
- こうすることで毎回sshする待ち時間が無くなる
- 多段sshだと待ち時間も馬鹿にならない
- 何よりAIに認証情報を渡す必要がない
- 人間が実行をリアルタイムに目視確認することが可能
- ヤバいコマンド打ち始めたら、
Ctrl+cで止めに入るのも可能 (間に合うかは別)
- ヤバいコマンド打ち始めたら、
- 人間とAIがシームレスに操作を交代することが可能
- 人間操作の実行内容・結果やログを渡す手間がない (一番最後の実践例参照)
- AIには、プロジェクトのAGENTS.mdやCLAUDE.mdで、最初にscreenセッションへアタッチするよう指示する
2. 環境
- Win11 25H2 + WSL2 + Ubuntu 24.04
- Codex
- Winアプリ 26.1002.52244
- エージェント環境:WSL
- Claude Code (Bedrock)
- claude 2.1.293。コマンドはWSL上に導入
- VSCodeの拡張機能から利用
- Node.js v24.21.0
- nvm 0.40.8
- npm 12.2.0
- ptyai 0.1.1
3. Claude Code用の準備
ここではClaude Code特有のセットアップ手順を解説する。Codexしか使わない人はスキップ。
3.1 Claude特有の問題点
screenはPTYを必要とする。
CodexはPTYを有効化するオプションがあり、AGENTS.mdで「PTYを有効化せよ」と書くだけでscreenが使える。
しかしClaudeは現状ではPTYを有効化することができない。
(Issueは挙がっているので、いつか実装されるかもしれない)
Issue:https://github.com/anthropics/claude-code/issues/9881
3.2 ptyai導入
そこで、PTYを提供するMCPを導入する。
ビルドに必要なので、下記が入っていなかったらインストールする。
sudo apt update
sudo apt install build-essential
ptyaiインストール。
npm install -g --allow-scripts=ptyai,node-pty ptyai
claude用の自動セットアップを実行。~/.claude/settings.json と~/.claude/CLAUDE.md に設定が追記される。
ptyai install
Proceed? [N/y] y
4. 事前準備 (共通)
Codex、Claude Code共通の事前準備手順。
4.1 変数定義
SCREEN_NAME="aiops" ## AIに渡すscreenセッションの名前
SSH_TARGET="test-aiops" ## ssh先ホスト
4.2 permanent-sshシェル関数を作成する (任意)
~/.bash_aliases等、各々の環境に合わせて下記を記述し、permanent-sshコマンドを使えるようにしておくと、場合によっては便利。
permanent-ssh() {
if [ -z "$1" ]; then
echo "Usage: permanent-ssh user@hostname"
return 1
fi
while true; do
ssh "$@"
echo "SSH session ended. Reconnecting in 3 seconds..."
sleep 3
done
}
4.3 AI用のscreenセッションを作成する
screen [-c ~/.screenrc-aiops] -[dm]S $SCREEN_NAME
## コピペ用
### 専用コンフィグでセッション作成してアタッチ
screen -c ~/.screenrc-aiops -S $SCREEN_NAME
### 専用コンフィグでセッションだけ作成
screen -c ~/.screenrc-aiops -dmS $SCREEN_NAME
### 標準コンフィグでセッション作成してアタッチ。screen初めての人はこれがおすすめ
screen -S $SCREEN_NAME
### 標準コンフィグでセッションだけ作成
screen -dmS $SCREEN_NAME
dmは、セッションの作成だけして、自動アタッチしないオプション。
必要に応じてc オプションで、AI用screenセッション専用の設定ファイルを読み込ませる。
自分の場合、下部にscreenのウィンドウタブをcaptionで表示させていたが、AIがプロンプトの応答と誤認すると面倒なので、普段使いの~/.screenrcからcaptionだけ除いた設定ファイルを作成した。
screenをカスタマイズしてない人は気にしなくて良い。
4.4 AI用screenセッションの中で作業対象にsshしておく
sshはたまに切れるので、自動で再接続させるために、先程作ったpermanent-sshコマンドで接続する。(任意。通常のsshでも可)
使えるオプションは通常のsshコマンドと同じ。
permanent-ssh $SSH_TARGET
4.5 AI用プロジェクト作成
AI用のプロジェクトを作成し、プロジェクト配下のAGENTS.mdやCLAUDE.mdに下記を記述する。
(後述)
5. 使い方と注意事項
5.1 screenの使い方
今回用のscreenの最低限の使い方解説。screen経験者はスキップ。
現在どんなscreenセッションがあるか確認するには下記。
$ screen -ls
There are screens on:
783928.aiops (10/06/26 18:07:58) (Detached)
145334.work (10/05/26 12:17:42) (Attached)
145208.proxy (10/05/26 12:17:27) (Detached)
アタッチ等でセッションを指定するときはaiops 等、セッション名だけ指定すればOK。頭の数字は省力可。
# アタッチの例
screen -rx aiops
現在、どのscreenセッションの中に居るのか確認するには下記。
~$ echo $STY
145334.work ## 空欄ならscreen外に居る
デタッチするときは、(カスタマイズしていなければ) Ctrl+a+d(Ctrlを押したままaとdを続けて入力) 。
exitするとscreenセッションが終了してしまうので注意。screenセッションが終了してしまったら、再度作成してsshまでしておく。
5.2 AIの操作を確認したり、人手の操作を割り込ませる
screenセッションを共有しているので、人手で対象セッションにアタッチすれば、AIの操作履歴を目視確認可能。
screen -rx $SCREEN_NAME
r はリタッチで、x は誰かがアタッチ中でもリタッチするオプション。こうすることでAIと人間が同時に操作可能。
アタッチしままにしておくと、AIの操作をリアルタイムに目視確認できる。
AIと人間がシームレスに操作を交代することも可能。(一番最後の実行例)
AIの操作を邪魔しないよう、AIがコマンドを実行している最中はキー入力しないこと。
5.3 screenの表示領域が狭い
下記画像のように、表示領域が狭いときは
Ctrl+a, Shift+f (Ctrlを押したままa。Ctrlから指を放し、Shiftを押したままf)
or
Ctrl+a, : (Ctrlを押したままa。Ctrlから指を放し、コロン) →fit(Enter)
とすると拡がる。
5.4 複数の会話セッションで同時にコマンドを実行しないこと
別の会話セッションでscreenにアタッチ済みでも、新しい会話セッションでは、最初のコマンド実行時に再度アタッチされる。
ただし本指針では、全会話セッションで、1つのscreenセッションを共有する。
複数の会話セッションで同時にコマンドを実行するとブッキングするため、それは避けること。
6. AIへの指示文
6.1 動作確認モデル
- Codex:GTP-6.1 Sol 中
- Claude Code:Sonnet5.5 中。実行モードはAuto
やってみた感じ、事前確認も事後報告も、Claude Codeの方が丁寧。
Claude Codeは事前に実行予定のコマンドを教えてくれるし、失敗したときの是正計画も承認を求めてくる。
一方Codexはいきなりコマンド実行し始めるし、失敗したときの是正も勝手に実行してしまって怖い。
6.2 AGENTS.md
プロジェクト内のAGENTS.mdを下記のようにする。
最初のaiops は適宜変更する。複数プロジェクトで並列作業するときは、screenセッションをプロジェクト毎に用意する。
## コマンド実行のルール
### 対象の定義
* 本プロジェクトの対象screenセッション名は`aiops`とする。
* 以下の「対象screenセッション」は、この名前のセッションを指す。
* プロジェクトごとに異なるセッション名を指定し、接続に使用する実行セッションIDとホスト名の比較基準をプロジェクト間で混同しない。
### 実行対象
* 本プロジェクトではrtkコマンドは使用しない。
* ユーザーは、作業対象へSSHログイン済みの対象screenセッションをローカルWSL上に事前に用意する。
* 本プロジェクトでユーザーがコマンドの実行を依頼した場合は、接続先の指定がなくても、対象screenセッション内の現在の接続先で実行する。
* 接続のためのscreen操作を除き、ユーザーから明示的な指示がない限り、依頼されたコマンドをローカルWSLや別の実行セッションで実行しない。
* 新しいSSH接続、接続先の変更、現在のSSH接続の終了は、ユーザーから明示的な指示がない限り行わない。
* SSH先の`$STY`でscreenへのアタッチ状態を判定しない。
### 接続の確保
* AIエージェントが対象screenセッションへの接続に使用した実行セッションIDを保持しており、その実行セッションが現在も利用可能である場合は、新しいシェルやscreen接続を起動せず、その実行セッションを再利用する。
* 実行セッションの確認には、まず追加入力を伴わない出力取得を使用する。セッションIDが存在することだけで、利用可能と判断しない。
* 利用可能な実行セッションを保持していない場合は、PTYを有効にし、ローカルWSL上でサンドボックス外から`screen -rx <対象screenセッション名>`を実行する。
`<対象screenセッション名>`には「対象の定義」で指定した名前を使用する。
* 接続に成功した場合は、返された実行セッションIDを以後の操作に使用する。
* 接続に失敗した場合や、接続先のシェルで操作できることを確認できない場合は、本来実行予定のコマンドを実行しない。
* 別の端末が対象screenセッションにアタッチしていることと、AIエージェントが利用可能な実行セッションを保持していることを混同しない。
* AIエージェントが利用可能な実行セッションを保持していない場合は、別の端末がアタッチ中でも、上記の接続コマンドで接続する。別の端末を強制デタッチする操作は行わない。
### 接続先の確認とコマンド実行
* 最初に`uname -n`を実行し、出力されたホスト名を以後の比較基準として保持する。
* 本来実行予定のコマンドを実行する前に、同じ実行セッション内で必ず`uname -n`を実行する。
* 依頼されたコマンド自体が`uname -n`の場合は、その実行結果を接続先の確認にも使用し、重複して実行する必要はない。
* ホスト名が比較基準と異なる場合、または結果を確認できない場合は、ユーザーに報告し、本来実行予定のコマンドは実行しない。
* 再接続した場合も、それまでの比較基準を引き継ぐ。再接続後のホスト名で比較基準を自動的に更新しない。
* ユーザーが接続先の変更を明示的に指示した場合は、変更後に`uname -n`を実行し、確認したホスト名を新しい比較基準として保持する。
* コマンド文字列を送信しただけでは実行完了と判断せず、実行結果とプロンプトの復帰、または終了状態を確認する。
* 通常のコマンド終了後も接続を保持する。ユーザーから明示的な指示がない限り、SSHの終了、screenの終了、デタッチは行わない。
* コマンド本文とEnterは分けて送信する。EnterにはCR文字を使用する。応答がない場合はコマンド全文を再送せず、入力済みの内容を確認する。
対象セッションとウィンドウを特定できた場合に限り、接続操作の代替手段としてscreenのstuff機能によるEnter送信を使用してよい。
実行結果とプロンプトの復帰を確認するまで、実行完了と判断しない。
毎回uname -nするのは、sshに失敗したり、sshが切れてローカルに戻ってるのを気付かず作業してしまうのを防ぐためのセーフティー。
uname -n の結果が変わったときにちゃんと止まるのは確認した。(AIなので保証はできないが)
AIが自分で実行したコマンドだけでなく、screenセッションに残っているログを遡って、状況確認してくれたりもする。
コマンド入力後、Enterを認識されない問題が発生したため、応答が無いときはscreenコマンドのstuffで個別にEnterを送るように指示している。
サンドボックス外でscrren -rx させるのは、サンドボックス内からscreenのソケットファイルにアクセスできないから。
ssh先を操作するときは、サンドボックスの中でも外でも結局同じなので、横着してこうした。
6.3 CLAUDE.md
不要だった。現在のclaudeはCLAUD.mdが無いときはAGENTES.mdを参照してくれる。
内容もそのままで意図通り動いた。
6.4 プロンプト例
以降は実際に試してみたプロンプトの内容とその結果。
参考情報なので、興味ある人だけどうぞ。
Codex実践例
gitをインストールして。
「サンドボックス外でscreenを実行して良いか?」だけ聞いてきたが、その後は勝手に作業し始めた。
[ec2-user@ip-10-53-100-5 ~]$ uname -n
ip-10-53-100-5.ap-northeast-1.compute.internal
[ec2-user@ip-10-53-100-5 ~]$ cat /etc/os-release; command -v git; git --version
NAME="Amazon Linux"
VERSION="2023"
ID="amzn"
ID_LIKE="fedora"
VERSION_ID="2023"
PLATFORM_ID="platform:al2023"
PRETTY_NAME="Amazon Linux 2023.12.20260930"
ANSI_COLOR="0;33"
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2023"
HOME_URL="https://aws.amazon.com/linux/amazon-linux-2023/"
DOCUMENTATION_URL="https://docs.aws.amazon.com/linux/"
SUPPORT_URL="https://aws.amazon.com/premiumsupport/"
BUG_REPORT_URL="https://github.com/amazonlinux/amazon-linux-2023"
VENDOR_NAME="AWS"
VENDOR_URL="https://aws.amazon.com/"
SUPPORT_END="2029-06-30"
-bash: git: command not found
[ec2-user@ip-10-53-100-5 ~]$ uname -n
ip-10-53-100-5.ap-northeast-1.compute.internal
[ec2-user@ip-10-53-100-5 ~]$ sudo -n dnf install -y git
(以下略)
Claude Code実践例
telnetをインストールして。
コマンド実行前に下記を確認してきた。👍️
対象screenセッション`aiops`内のSSH接続先に、telnetをインストールします。コマンドはまだ実行していません。実行前に承認をお願いします。
**実行手順(すべて未実行)**
1. ローカルWSLで`screen -rx aiops`に接続します。ここは接続操作だけで、接続先には変更を加えません。
2. `uname -n`でホスト名を確認し、以後の比較基準にします。
3. `cat /etc/os-release`でOSを確認します。読み取りのみです。
4. OSに応じて、次のどれかを実行します。このステップだけが変更になります。
- Amazon Linux、RHEL系: `sudo dnf install -y telnet`(古い環境では`yum`)
- Debian、Ubuntu系: `sudo apt-get install -y telnet`
**目的と影響**
- 目的は、接続先にtelnetクライアントを導入することです。
- 影響は、telnetパッケージとその依存パッケージが追加されることです。パッケージリポジトリへの外部通信が発生し、sudo権限が必要です。
- 既存のサービスや設定は変更しません。再起動も不要です。
- telnetはクライアントのみで、telnetサーバーは起動しません。
この内容で進めてよいか、ご承認ください。
調査系
Claude Codeに依頼
直近3日間の起動停止と、ユーザログインのレポートを作成して。
実行計画や報告方法をヒアリングしてくるので、適宜回答。
調査の様子。
[ec2-user@ip-10-53-100-5 ~]$ uname -n
ip-10-53-100-5.ap-northeast-1.compute.internal
[ec2-user@ip-10-53-100-5 ~]$ date; uptime -s
Thu Oct 8 03:57:10 UTC 2026
2026-10-08 01:58:18
[ec2-user@ip-10-53-100-5 ~]$ uname -n; last -x -F -s "$(date -d '3 days ago' '+%Y-%m-%d %H:%M:%S')"
ip-10-53-100-5.ap-northeast-1.compute.internal
ec2-user pts/1 127.0.0.1 Thu Oct 8 03:29:57 2026 still logged in
ec2-user pts/0 127.0.0.1 Thu Oct 8 03:27:18 2026 still logged in
runlevel (to lvl 5) 6.18.51-120.163. Thu Oct 8 01:58:54 2026 still running
reboot system boot 6.18.51-120.163. Thu Oct 8 01:58:17 2026 still running
shutdown system down 6.18.51-120.163. Wed Oct 7 08:20:02 2026 - Thu Oct 8 01:58:17 2026 (17:38)
runlevel (to lvl 5) 6.18.51-120.163. Wed Oct 7 02:44:14 2026 - Wed Oct 7 08:20:02 2026 (05:35)
ec2-user pts/0 127.0.0.1 Wed Oct 7 02:44:09 2026 - Wed Oct 7 08:07:31 2026 (05:23)
reboot system boot 6.18.51-120.163. Wed Oct 7 02:43:37 2026 - Wed Oct 7 08:20:02 2026 (05:36)
shutdown system down 6.18.51-120.163. Tue Oct 6 10:27:25 2026 - Wed Oct 7 02:43:37 2026 (16:16)
ec2-user pts/0 127.0.0.1 Tue Oct 6 09:19:15 2026 - Tue Oct 6 10:27:17 2026 (01:08)
ec2-user pts/1 127.0.0.1 Tue Oct 6 07:33:35 2026 - Tue Oct 6 08:48:57 2026 (01:15)
ec2-user pts/0 127.0.0.1 Tue Oct 6 07:29:52 2026 - Tue Oct 6 07:38:38 2026 (00:08)
runlevel (to lvl 5) 6.18.51-120.163. Tue Oct 6 07:29:02 2026 - Tue Oct 6 10:27:25 2026 (02:58)
reboot system boot 6.18.51-120.163. Tue Oct 6 07:28:25 2026 - Tue Oct 6 10:27:25 2026 (02:59)
wtmp begins Tue Oct 6 07:28:25 2026
[ec2-user@ip-10-53-100-5 ~]$ uname -n; sudo journalctl --list-boots --no-pager
ip-10-53-100-5.ap-northeast-1.compute.internal
IDX BOOT ID FIRST ENTRY LAST ENTRY
-2 5a4eafc235c747eea01f6632779b2d38 Tue 2026-10-06 07:28:24 UTC Tue 2026-10-06 10:27:25 UTC
-1 a46a2f8a10714083a7f070ce2c2af60a Wed 2026-10-07 02:43:36 UTC Wed 2026-10-07 08:20:03 UTC
0 fbeba0a72e234ab98f57db8a335aed05 Thu 2026-10-08 01:58:16 UTC Thu 2026-10-08 03:57:25 UTC
[ec2-user@ip-10-53-100-5 ~]$ uname -n; sudo journalctl -u sshd --since "3 days ago" --no-pager | grep -E "Accepted|Failed|Invalid"
ip-10-53-100-5.ap-northeast-1.compute.internal
Oct 06 07:29:52 ip-10-53-100-5.ap-northeast-1.compute.internal sshd-session[2068]: Accepted publickey for ec2-user from 127.0.0.1 port 47146 ssh2: ED25519 SHA256:...
Oct 06 07:33:35 ip-10-53-100-5.ap-northeast-1.compute.internal sshd-session[2248]: Accepted publickey for ec2-user from 127.0.0.1 port 51462 ssh2: ED25519 SHA256:...
Oct 06 09:19:15 ip-10-53-100-5.ap-northeast-1.compute.internal sshd-session[5626]: Accepted publickey for ec2-user from 127.0.0.1 port 53876 ssh2: ED25519 SHA256:...
Oct 07 02:44:08 ip-10-53-100-5.ap-northeast-1.compute.internal sshd-session[1966]: Accepted publickey for ec2-user from 127.0.0.1 port 49296 ssh2: ED25519 SHA256:...
Oct 08 03:27:17 ip-10-53-100-5.ap-northeast-1.compute.internal sshd-session[4708]: Accepted publickey for ec2-user from 127.0.0.1 port 51102 ssh2: ED25519 SHA256:...
Oct 08 03:29:57 ip-10-53-100-5.ap-northeast-1.compute.internal sshd-session[4891]: Accepted publickey for ec2-user from 127.0.0.1 port 35638 ssh2: ED25519 SHA256:...
[ec2-user@ip-10-53-100-5 ~]$
良い感じのレポートが出てきたけど、長くなるし趣旨違いなので割愛。
AIと人間が交互に操作
Claude Codeに依頼
`su - testuser`を実行して。
[ec2-user@ip-10-53-100-5 ~]$ uname -n
ip-10-53-100-5.ap-northeast-1.compute.internal
[ec2-user@ip-10-53-100-5 ~]$ su - testuser
Password: # ここで入力待ちになる
`su - testuser`を実行し、`Password:`の入力待ちになっています。
- ホスト名は`uname -n`で基準と一致しています。
- パスワードは私から入力しません。あなたの端末から入力してください。
- パスワードを入力したら教えてください。画面でログイン結果を確認します。
- 認証が失敗した場合は、プロンプトに戻ったことを確認して報告します。
- 切り替わった場合は、プロンプトが`[testuser@...]`に変わります。その状態でのコマンド実行は、指示があるまで行いません。
Password: ## 人手でパスワードを入力してEnter
Last login: Thu Oct 8 04:20:56 UTC 2026 on pts/0
[testuser@ip-10-53-100-5 ~]$
入力した。
`su - testuser`は成功し、プロンプトが`[testuser@ip-10-53-100-5 ~]$`に変わりました。
- 認証は通りました。パスワードは私は見ておらず、画面にも表示されていません。
- ホスト名は`ip-10-53-100-5`のままで、同じ接続先です。
- 画面に`Last login: Thu Oct 8 04:20:56 UTC 2026 on pts/0`と出ています。この`testuser`は、約1分前にも一度ログインされています。
- `ec2-user`のシェルの上にsuで入った状態です。`exit`で`ec2-user`に戻りますが、`exit`は指示があるまで送りません。
次の指示をお待ちします。以降のコマンドは`testuser`として実行されます。
