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か
ターミナルの永続化といえば tmux や screen だが、これらはターミナルエミュレーションを自前で行う。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メールリンク方式
パスワードを持たない認証方式を採用。
- ログイン画面でメールアドレスを入力
- サーバーがワンタイムトークン(crypto.randomBytes(32))を生成
- メールでログインリンクを送信(有効期限15分)
- リンクをクリックするとトークンを検証、セッション確立
- 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のプロキシ設定(RewriteRuleでws://へのプロキシ)も必要。また、リバースプロキシ経由で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入力バー
「入力」ボタンで日本語入力専用のテキストフィールドを表示(キーバーは自動的に非表示になり画面を確保):
- 日本語をIMEで入力(音声入力も可能)
- 「送信」でテキスト送信
- 「送信+⏎」でテキスト送信+Claude Codeに実行指示
- 送信後にソフトキーボードが自動的に閉じる
テキスト選択(Selボタン)
iPhoneではxterm.jsのcanvas上でタッチ選択ができないため、「Sel」ボタンでターミナルの内容をテキストオーバーレイに表示。長押しでネイティブ選択→コピーが可能。
タブタイトル
ブラウザのタブにセッション名を表示。セッション一覧でリネームするとBroadcastChannelで即座に反映。
タブ切り替え時の自動フォーカス
ブラウザタブを切り替えて戻った時に、自動でターミナルにフォーカスが入る(いちいちクリックしなくて済む)。
インストール
Debian/Ubuntu、RHEL/Rocky Linux対応のワンライナーインストーラー。
# 必要なもの:Linux サーバー + root権限
# Node.js 20+、abduco、その他依存は自動インストール
bash install.sh
インストーラーが行うこと:
- OS判定(apt/dnf/yum自動選択)
- 依存パッケージインストール(abduco、Node.js 20等)
- ロケール設定(ja_JP.UTF-8、なければ自動インストール)
- npm install
- .env設定(SMTP、ポート等)
- DB初期化 + 管理者ユーザー登録
- systemdサービス登録・起動(KillMode=process)
-
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