0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude CodeをブラウザからiPhone/iPad等(多分泥もいけるべ)で操作できるWebTerminalを作った

0
Posted at

TL;DR

  • ブラウザからサーバーのターミナルに接続できるWebアプリを作った
  • Claude Codeが表示崩れなしで動く(tmuxでは崩れる問題を解決)
  • iPhne/iPadからClaude Codeに日本語音声入力で指示が出せる
  • SSHセッションをブラウザに引き継げる
  • ファイルのD&Dアップロード、クリップボード画像ペースト対応
  • セッションのグルーピングと並び替え
  • HTTPS対応で右クリック/Ctrl+V/Alt+Vペースト
  • ワンライナーでインストール可能

背景:なぜ作ったのか

Claude Codeをリモートサーバーで使いたい。でも:

  • TeraTermだと表示が崩れる — Claude Codeのエスケープシーケンスが一部正しく表示されない、画面にゴミが残る
  • tmuxだと完全に崩壊 — tmuxが自前でターミナルエミュレーションするため、Claude Codeの複雑なエスケープシーケンスが変換されて画面がめちゃくちゃに
  • iPadから使いたい — 外出先でiPadしかない時にClaude Codeを操作したい
  • セッションを永続化したい — Claude Codeのコンテキストは貴重。ブラウザを閉じても、PCを変えても、同じセッションに戻りたい

技術選定:なぜabducoか

ターミナルの永続化といえば tmuxscreen だが、これらはターミナルエミュレーションを自前で行う。Claude Codeはkittyプロトコルなど高度なエスケープシーケンスを使うため、変換されると表示が崩れる。

最初はdtachを使っていたが、大量のターミナル出力時にバッファの問題でデータが欠落する問題があり、abduco(dtachの改良版)に切り替えた。abducoはPTYを素通しするだけのセッションマネージャーで、バッファ管理が改善されている。

技術 PTY処理 Claude Code バッファ
tmux 自前エミュレーション 表示崩壊 -
screen 自前エミュレーション 表示崩壊 -
dtach パススルー OK 欠落あり
abduco パススルー(バッファ改善) 完璧 OK

アーキテクチャ

[ブラウザ/iPad/iPhone]
    |
    | WebSocket (wss)
    |
[Apache/nginx (SSL)] ← リバースプロキシ
    |
[Node.js + Express :3000]
    |
    | node-pty
    |
[abduco] ←── セッション永続化(独立デーモン)
    |
[login / ssh user@host]
    |
[bash / Claude Code]

主要コンポーネント

レイヤー 技術 役割
フロントエンド xterm.js 4.19.0 ターミナルエミュレーション
通信 WebSocket (wss) リアルタイム双方向通信
バックエンド Node.js + Express HTTPサーバー + WebSocket
PTY node-pty 疑似ターミナル生成
セッション永続化 abduco PTYパススルー
DB better-sqlite3 ユーザー・セッション・認証情報管理
認証 OTPメールリンク ワンタイムログイン
暗号化 AES-256-GCM サーバー認証情報の暗号化保存
SSL Let's Encrypt HTTPS + クリップボードAPI

認証:OTPメールリンク方式

パスワードを持たない認証方式を採用。

  1. ログイン画面でメールアドレスを入力
  2. サーバーがワンタイムトークン(crypto.randomBytes(32))を生成
  3. メールでログインリンクを送信(有効期限15分)
  4. リンクをクリックするとトークンを検証、セッション確立
  5. Cookie(maxAge 30日、rolling)で以降は再認証不要

ユーザーは管理者が事前登録制(wt useradd user@example.com)。野良登録はできない。同一IPからのリクエストは1分5回までレート制限。

セキュリティ:認証情報の暗号化

サーバー接続用のユーザー名/パスワードは、保存時にユーザーが入力するマスターパスワードで暗号化される。

[保存時]
ユーザー入力: サーバーパスワード + マスターパスワード
    → PBKDF2(マスターパスワード, ランダムsalt) → AES鍵導出
    → AES-256-GCM暗号化 → DBに保存

[使用時]
ユーザー入力: マスターパスワード
    → AES鍵導出 → 復号 → サーバーに自動送信 → メモリから即削除
  • マスターパスワード自体はサーバーに一切保存しない
  • 暗号化パスワードは各エントリごとにランダムなsalt+IVを使用
  • 復号されたパスワードはメモリ上に一時保持(60秒で自動消去)、使用後即削除
  • 間違ったマスターパスワードではAES-GCMの認証タグ検証で復号が失敗し、nullが返る

苦労した点と解決策

1. 日本語表示の文字化け

問題: Buffer.from(data, 'binary') でPTYの出力をWebSocketに送っていたら、日本語が文字化けした。

原因: 'binary'(= latin1)エンコーディングはマルチバイト文字の上位バイトを切り捨てる。

解決: 文字列をそのままws.send(data)で送信。

// NG
ws.send(Buffer.from(data, 'binary'));

// OK
ws.send(data);

2. テキスト選択ができない

問題: xterm.jsの画面上でマウスドラッグしてもテキストが選択できない。

原因: xterm.min.cssが読み込まれていなかった。xterm.jsはこのCSSがないと選択レイヤーが描画されず、マウスイベントも正しく処理されない。

解決: <link rel="stylesheet" href="/vendor/xterm.min.css">を追加。単純だが発見に時間がかかった。

3. マウスホイールで入力履歴がスクロールされる

問題: マウスホイールを回すとターミナルアプリにマウスイベントが送られ、claudeコマンド入力履歴が前後した。

解決: サーバー側でマウスレポーティングとalternate bufferの有効化シーケンスをフィルタリング。

const filterRe = /\x1b\[\?(1000|1002|1003|1004|1005|1006|1015|1016|1049|47)h/g;
ptyProcess.onData((data) => {
  ws.send(data.replace(filterRe, ''));
});

マウスレポーティングをフィルタすることでテキスト選択が可能に、alternate buffer(\x1b[?1049h)をフィルタすることでスクロールバックも有効に。クライアント側ではdocumentレベルのキャプチャフェーズでwheelイベントをインターセプトし、term.scrollLines()でxterm.jsのバッファスクロールに変換。

4. systemctl restartでセッションが死ぬ

問題: systemctl restart webterminalするとabducoのセッションも全部死ぬ。

原因: systemdのデフォルトKillMode=control-groupが、cgroup内の全プロセスをkillする。abducoがabduco -nでデーモン化しても、同じcgroupに属しているため一緒に殺される。

解決:

# webterminal.service
[Service]
KillMode=process

KillMode=processにすることで、メインプロセス(Node.js)だけがkillされ、abducoのデーモンプロセスは生き残る。

5. iPadの古いChromeでの互換性

問題: iPad Chrome 92(iOS 14)で動かない(過去の呪物の有効活用)。

原因: 複数の問題が重なっていた:

  • BroadcastChannelが未サポート → try-catchで囲む
  • async/awaitが未サポート → .then()チェーンに書き換え
  • CDN(jsdelivr)がブロック → xterm.jsをローカルホスト
  • xterm.js 5.xが未サポート → 4.19.0にダウングレード

教訓: iOS版ChromeはWebKitベースなので、実質Safari。Chrome固有のAPIは使えない。

6. iPadの日本語IME入力が二重になる

問題: iPadで日本語入力すると「たた」「正しく正しく」のように文字が二重に入力される。

原因: xterm.jsがIMEの変換途中のキーイベントも即座にPTYに送ってしまう(恐らく呪物の処理速度の問題)。

解決: 専用のIME入力バーを追加。テキストボックスで日本語を確定してから、1文字ずつ10ms間隔でPTYに送信。

// 一括送信 → Claude Codeがペーストと判断し複数行入力モードに
ws.send(text); // NG: Enterが改行になる

// 1文字ずつ送信 → 通常入力として認識される
function sendNext() {
  if (i < text.length) {
    ws.send(text.charAt(i));
    i++;
    setTimeout(sendNext, 10);
  }
}

Claude Codeは一括テキストを「ペースト」と判断して複数行入力モードに入るため、Enterが送信ではなく改行になる。1文字ずつ送ることで通常入力モードを維持できる。これにより「送信+⏎」ボタンで日本語テキスト送信→自動Enter送信が実現。

7. 右クリック/ペーストにHTTPSが必要

問題: 右クリックペーストやAlt+Vペースト、クリップボード画像ペーストが動かない。

原因: navigator.clipboard.readText()navigator.clipboard.read()はSecure Context(HTTPS)でないとブラウザにブロックされる。

解決: Let's Encrypt + Apache/nginxリバースプロキシでHTTPS化。WebSocketのプロキシ設定(RewriteRulews://へのプロキシ)も必要。また、リバースプロキシ経由でCookieのsecure属性を正しく動かすため、RequestHeader set X-Forwarded-Proto "https"trust proxy設定が必須。

# Apache 443 VirtualHost
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/

# WebSocket proxy
RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule /(.*) ws://127.0.0.1:3000/$1 [P,L]

HTTP環境でのフォールバックとして、一時的なテキストエリアを表示してCtrl+Vでペーストする機能も実装。

8. Ctrl+Vで画像かテキストかを判別

問題: Ctrl+Vがxterm.jsに食われてクリップボード画像のアップロードができない。

解決: attachCustomKeyEventHandlerでCtrl+Vをインターセプトし、navigator.clipboard.read()でクリップボードの内容を確認。画像があればアップロード、テキストのみならペースト。

if (ev.ctrlKey && !ev.shiftKey && ev.key === 'v') {
  navigator.clipboard.read().then(function(items) {
    // 画像があればアップロード、なければテキストペースト
  });
  return false;
}

SSHセッション共有:wtコマンド

ブラウザとSSHでセッションを共有できるCLIツールも作った。

# SSHからセッション作成
wt

# Claude Code起動
claude

# Ctrl+\ でデタッチ(セッションは生存)

# ブラウザのWebTerminalからセッション一覧に表示される
# 「接続」ボタンで同じセッションにアタッチ

これにより:

  • SSHで始めた作業をiPadに引き継ぎ
  • iPadで始めた作業をPCに引き継ぎ
  • 複数デバイスから同じセッションを確認
  • TeraTermを閉じてもabducoデーモンが独立して生存

ユーザー管理もwtコマンドから:

wt users          # ユーザー一覧
wt useradd user@example.com  # ユーザー追加
wt userdel user@example.com  # ユーザー削除
wt ls             # セッション一覧
wt a <id>         # セッションにアタッチ

認証情報管理

サーバーのログイン情報を暗号化して保存し、ワンクリックで接続できる機能。全てログインユーザーに紐づき、他のユーザーからは見えない。

  • ローカルサーバー:login + ユーザー名/パスワード自動入力
  • リモートサーバー:SSH接続 + パスワード自動入力(StrictHostKeyChecking=accept-newで初回ホスト鍵も自動受入)
  • パスワードはマスターパスワードでAES-256-GCM暗号化して保存(詳細は上記「セキュリティ」セクション参照)

セッション一覧画面から「+ ローカル」「+ リモート」で保存済みの認証情報を選択、マスターパスワードを入力するだけで新しいタブで接続完了。認証情報の追加・編集・削除は「⚙ 認証管理」から。

セッションのグルーピングと並び替え

セッションが増えると管理が大変になるため、グループ管理機能を実装。

  • グループ作成:「+ グループ」ボタンで名前付きグループを作成
  • 折りたたみ:グループヘッダーをクリックで展開/折りたたみ(セッション数表示付き)
  • セッション移動:「移動」ボタンで任意のグループに移動、新規グループ作成しながら移動も可能
  • 並び替え:▲▼ボタンでセッション・グループの表示順を変更
  • グループ削除:グループを削除してもセッションは「未分類」に移動(消えない)

業務別、サーバー別にグループ分けすることで、多数のセッションを整理して管理できる。

ファイル転送

ターミナル画面から直接ファイルのアップロード・ダウンロードが可能。

アップロード(3つの方法)

  • ドラッグ&ドロップ — ファイルをターミナル画面にD&D(オーバーレイ表示あり)
  • クリップボード画像ペースト — スクリーンショット等をCtrl+Vで自動アップロード(画像を検知して自動判別)
  • Uploadボタン — ファイル選択ダイアログからアップロード

アップロード完了後、ターミナルにファイルパスが表示される。Claude Codeへの画像指示にそのまま使える。

[Upload OK] /tmp/webterminal-uploads/20260329_030413_screenshot.png

ダウンロード

  • Downloadボタン → ファイルパス入力 → ブラウザでダウンロード

iPad/iPhone対応

モバイルデバイスでの操作に特化したUIを実装。

エミュレーションキーバー

ソフトキーボードにないキーをタップで入力:

Esc | Tab | S+Tab | ↑↓←→ | Ctrl | C-c | C-d | C-z | C-a | ⏎ | C-⏎ | ⤓ | Sel | Up | Dn | 入力 | Logout

IME入力バー

「入力」ボタンで日本語入力専用のテキストフィールドを表示(キーバーは自動的に非表示になり画面を確保):

  1. 日本語をIMEで入力(音声入力も可能)
  2. 「送信」でテキスト送信
  3. 「送信+⏎」でテキスト送信+Claude Codeに実行指示
  4. 送信後にソフトキーボードが自動的に閉じる

テキスト選択(Selボタン)

iPhoneではxterm.jsのcanvas上でタッチ選択ができないため、「Sel」ボタンでターミナルの内容をテキストオーバーレイに表示。長押しでネイティブ選択→コピーが可能。

タブタイトル

ブラウザのタブにセッション名を表示。セッション一覧でリネームするとBroadcastChannelで即座に反映。

タブ切り替え時の自動フォーカス

ブラウザタブを切り替えて戻った時に、自動でターミナルにフォーカスが入る(いちいちクリックしなくて済む)。

インストール

Debian/Ubuntu、RHEL/Rocky Linux対応のワンライナーインストーラー。

# 必要なもの:Linux サーバー + root権限
# Node.js 20+、abduco、その他依存は自動インストール
bash install.sh

インストーラーが行うこと:

  1. OS判定(apt/dnf/yum自動選択)
  2. 依存パッケージインストール(abduco、Node.js 20等)
  3. ロケール設定(ja_JP.UTF-8、なければ自動インストール)
  4. npm install
  5. .env設定(SMTP、ポート等)
  6. DB初期化 + 管理者ユーザー登録
  7. systemdサービス登録・起動(KillMode=process)
  8. wtコマンドのインストール

HTTPS化(推奨)

Apache/nginxのリバースプロキシ + Let's EncryptでHTTPS化すると:

  • クリップボードAPI有効化(右クリックペースト、Alt+Vペースト、Ctrl+V画像アップロード)
  • Cookieのsecure属性が有効に
  • 通信の暗号化

まとめ

機能 対応
Claude Code表示 崩れなし(abducoパススルー)
セッション永続化 ブラウザ閉じても、サーバー再起動しても生存
マルチデバイス PC、iPad、iPhone、スマホからアクセス
SSH共有 wtコマンドでSSH↔ブラウザ引き継ぎ
日本語入力 IME入力バー + 音声入力対応
テキスト選択 PC: マウスドラッグ / モバイル: Selオーバーレイ
コピー&ペースト Ctrl+Shift+C/V、Ctrl+V、右クリック、Alt+V
スクロール マウスホイールでスクロールバック(10000行)
ファイル転送 D&D、クリップボード画像、Upload/Download
認証情報管理 暗号化保存、自動ログイン(ローカル/SSH)
セッション整理 グルーピング、折りたたみ、並び替え
HTTPS Let's Encrypt + リバースプロキシ
インストール ワンライナー

既存のWebターミナル(ttyd、Wetty等)ではClaude Codeが正常に動かない。 tmux依存やエスケープシーケンスの変換が原因。abducoのPTYパススルー方式でしか解決できないため、このWebTerminalには独自の価値がある(はず)。

外出先でiPadからClaude Codeに音声で日本語指示を出し、コードを書かせる。移動中にiPhoneから作業の進捗を確認する。その快適さは、一度体験すると戻れない。
興味のある方連絡ください。

そして、出来上がってから気付いた/remote-controlスキルは内緒ですorz

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?