結論:2つの別問題として切り分ける
「文字化け」と「日本語が打てない」は、原因も対処もまったく別の問題です。 混ぜて考えると解決しません。
文字化け(表示側)は設定で直ります。 原因のほぼ全ては、サーバー側のロケールが UTF-8 になっていないことです。サーバーで locale を実行して LANG=C や POSIX が返ってきたら、~/.bashrc に export LANG=C.UTF-8 を1行足すだけで直ります。C.UTF-8 はどのディストリビューションにも最初から存在するので、locale-gen すら要りません。
日本語入力(入力側)は設定では直りません。 スマホの SSH アプリで変換候補が出ない、Space を押すとシェルに空白が入る、Enter が確定ではなくコマンド実行になる ── これはアプリが IME の未確定文字列(iOS の marked text / Android の composing text)を自前で処理していないために起きる構造上の問題で、ユーザー側にスイッチはありません。対処は回避策になります。
見分け方は単純です。サーバーが出した日本語が崩れているなら表示側の問題、自分が打った日本語が崩れるなら入力側の問題です。両方同時に起きている場合は、先に表示側を直してください。表示が正しくなると、入力側の症状が正確に読めるようになります。
以下、症状別の切り分け表、UTF-8 の揃え方、豆腐と幅ズレ、IME 問題の中身と回避策、ファイル名の文字化け、そして Shift_JIS 時代のファイルの扱いを順に説明します。
30秒で切り分ける:症状別の原因
画面に出ている「崩れ方」で原因はほぼ確定します。まず接続先のサーバーで次の2行を実行してください。
locale
printf 'あ' | od -An -tx1
2行目が e3 81 82 を返せば、バイト列は正しい UTF-8 です。つまりサーバーもネットワークも正常で、問題は表示側(フォントか幅)にあります。違うバイト列が返る、あるいはコマンド自体が化ける場合は、エンコーディングかロケールの問題です。
| 見えているもの | 実際に起きていること | 直す場所 |
|---|---|---|
日本語 のようなラテン文字の羅列 | UTF-8 のバイト列を Latin-1 として描画している | ターミナル側のエンコーディング設定を UTF-8 に |
<E3><81><82> や \346\227\245 | LANG=C のため less / ls がバイトをエスケープ表示 | サーバー側のロケール(次節) |
???? や �� | ロケール不一致、または不正なバイト列 | サーバー側のロケール、または iconv |
□□□(同じ形の四角が並ぶ) | フォントに日本語グリフがない(豆腐) | アプリのフォント設定 |
| 文字は読めるが罫線や表がズレる | East Asian Ambiguous Width の不一致 | ターミナルの「あいまい幅」設定 |
| 日本語部分の削除でカーソルが半分ずれる | バックスペースが1バイト単位で効いている | アプリ側の実装(入力側の問題) |
| 打った日本語だけ化ける、サーバーの出力は正常 | IME の未確定文字列がそのまま送られている | アプリの IME 対応(設定では直らない) |
最後の2行が入力側、それ以外が表示側です。表示側は全部この記事の手順で直ります。
文字化けを直す:UTF-8 を両端で揃える
サーバー側:ロケールを UTF-8 にする
locale の出力が LANG=C、LANG=POSIX、あるいは全部空なら、そのシェルは日本語を「意味のないバイト列」として扱っています。最短の対処はこれです。
echo 'export LANG=C.UTF-8' >> ~/.bashrc
C.UTF-8 を勧める理由は、生成が不要だからです。 C.UTF-8 はロケール一覧に常に存在する既定のフォールバックで、locales パッケージも locale-gen も要りません。日本語の表示・ファイル名の扱いに必要なのは「バイト列を UTF-8 として解釈すること」だけで、それは C.UTF-8 で足ります。
日本語のエラーメッセージ、日本語の日付書式、日本語のソート順まで欲しい場合だけ ja_JP.UTF-8 を使います。こちらは明示的な生成が必要です。
sudo apt install -y locales
sudo locale-gen ja_JP.UTF-8
sudo update-locale LANG=ja_JP.UTF-8
コンテナや最小構成のクラウドイメージでは locales パッケージ自体が入っていないことが多いので、1行目を飛ばさないでください(Ubuntu のロケール設定)。
cannot change locale 警告の正体
ログイン直後にこれが出ることがあります。
-bash: warning: setlocale: LC_CTYPE: cannot change locale (ja_JP.UTF-8): No such file or directory
これは「クライアントが送ってきたロケール名が、サーバーに存在しない」という意味です。OpenSSH クライアントは Debian / Ubuntu / macOS が配布する /etc/ssh/ssh_config で SendEnv LANG LC_* を設定しており、これらのディストリビューションの sshd_config は AcceptEnv LANG LC_* でそれを受け取ります(sshd_config(5))。つまり手元のロケール名がそのままリモートのシェルに渡る設計です。上流 OpenSSH の AcceptEnv の既定値は「受け取らない」なので、この現象はディストリビューション側の設定に由来します。
対処は「実際に生成する」か「送らない」かの二択です。
# 送らない側で解決する
LC_ALL=C.UTF-8 ssh user@host
macOS から接続している場合の定番の罠がこれで、Terminal.app が LC_CTYPE=UTF-8 を送ることがありますが、UTF-8 は正当なロケール名ではないため、どのサーバーでもこの警告が出ます。
警告そのものは無害ですが、放置すると LC_CTYPE が C にフォールバックし、結果として日本語が化けます。 「警告は出るが日本語は読める」という状態はあまり起きないので、出ていたら直してください。
スマホの SSH アプリ特有の注意
ここがデスクトップと違う点です。モバイルの SSH クライアントは OpenSSH ではないため、LANG を送らないことが多いです。ホストごとの「環境変数」欄がなければ、サーバーのログインシェルが設定した値がそのまま使われます。
したがって、スマホから接続する場合の対処は基本的にサーバー側になります。~/.bashrc に export LANG=C.UTF-8 を書く方法が、どのアプリを使っても効く唯一の方法です。アプリ側に「文字エンコーディング」の設定があれば UTF-8 になっているかも確認してください(古い設定を引きずって Shift_JIS や Latin-1 になっていることがあります)。
tmux を使っている場合の落とし穴
tmux は起動時に LC_ALL → LC_CTYPE → LANG の順に見て、最初に設定されている変数に UTF-8 という文字列が含まれるかどうかで UTF-8 対応を判定します。utf8 と status-utf8 のオプションは tmux 2.2(2016年)で削除され、現在は完全な自動判定です(tmux CHANGES)。
問題は、tmux サーバーは一度起動すると生き続けることです。ロケールを直す前に起動した tmux セッションに tmux attach で戻ると、古い判定のまま動き続けます。
tmux kill-server # ロケールを直した後、必ず一度落とす
これを知らずに「.bashrc を直したのに化けたまま」で時間を溶かす人がとても多いところです。長時間動かしっぱなしのセッションについては SSH 切断後もプロセスを生かす も参照してください。
less / ls / vim の個別対処
ロケールを直せば大半は追随しますが、残るものがあります。
lessが<E3><81><82>と表示する ──export LESSCHARSET=utf-8を追加します。ロケールがCのときにlessが非 ASCII バイトをエスケープする挙動です。lsが?や\346\227\245を出す ── これもロケール由来です。一時的に見たいだけならls --show-control-charsで通せます。vimで既存ファイルが化ける ── 読み込み時の推測順を明示します。
set encoding=utf-8
set fileencodings=utf-8,cp932,euc-jp,iso-2022-jp
その場だけなら :e ++enc=cp932 で開き直せます。順序が重要で、utf-8 を先頭に置かないと UTF-8 のファイルが CP932 と誤判定されることがあります。
豆腐(□□□)と幅ズレを直す
ロケールを直しても四角が並ぶなら、それは文字化けではありません。フォントにその文字の字形が入っていないだけです。
判定は前述の printf 'あ' | od -An -tx1 で足ります。e3 81 82 が返っているのに四角が見えているなら、確定でフォントの問題です。
スマホの SSH アプリで起きやすい理由ははっきりしています。等幅の「コーディング用フォント」── JetBrains Mono、Fira Code、SF Mono など ── には漢字・かな・全角記号が1文字も入っていません。日本語部分は OS のフォールバック(iOS なら Hiragino Sans、Android なら Noto Sans CJK JP)に回るのが正常な動作ですが、アプリがフォールバックを実装していなければ豆腐になります。アプリのフォント一覧に日本語対応フォントがあるならそれを選ぶのが確実です。
East Asian Ambiguous Width(あいまい幅)
文字は読めるのに htop の枠や表の罫線が1桁ずつずれる場合、これが原因です。
Unicode の UAX #11: East Asian Width は、①②③、※、○、●、★、±、÷、ギリシャ文字、キリル文字などを Ambiguous(あいまい) に分類しています。これらは東アジアのレガシー文字集合では全角文字として、それ以外の用途では半角文字として使われてきた文字で、「どちらが正しい」という規定がありません。
結果として、ターミナル側が「1桁」と判断し、サーバー側の wcwidth() が「2桁」と返すと、その文字が出るたびに表示が1桁ずつずれていきます。TUI アプリが罫線を引いた瞬間に崩壊します。
対処は「両端の判断を揃える」ことです。
- ターミナル側で揃える ── アプリに「あいまい文字を全角として扱う」「Ambiguous width: double」といった設定があれば、日本語を多く扱うなら全角側に寄せます。
- サーバー側で揃える ── あいまい文字を全角と宣言する修正ロケール(hamano/locale-eaw など)を入れる方法があります。サーバーを自分で管理していて、複数人が同じ端末設定を使う場合に有効です。
- そもそも避ける ── スマホの画面幅では、これが一番現実的です。
htopの代わりにtop -b -n1、全画面のページャの代わりにjournalctl --no-pagerと、全画面 TUI ではなく行出力に寄せると、幅の食い違いが表示崩れに化けません。狭い画面での運用の組み立て方は スマホからサーバーを管理する にまとめています。
なぜスマホの SSH アプリで日本語が打てないのか
ここからが入力側です。これは設定では直りませんが、理由を知っておくとアプリ選びと回避策の判断が一瞬で終わります。
未確定文字列という仕組み
日本語 IME は、キーを打つたびに文字を確定しません。「にほんご」と打っている間、その文字列は未確定(下線付き)状態でアプリに預けられ、変換候補から選んで確定した時点ではじめて本文に入ります。
OS はこれを専用の API で扱います。
- iOS ──
UITextInputプロトコルのsetMarkedText:selectedRange:が未確定文字列をアプリに渡し、確定時にinsertText:が呼ばれます。 - Android ──
InputConnectionのsetComposingText()が同じ役割を担い、finishComposingText()で確定します。
つまりアプリ側には、**「未確定の文字列を画面に表示しつつ、まだ送信しない」**という状態管理が必要です。
ターミナルエミュレータはテキストフィールドではない
ここが衝突点です。ターミナルエミュレータの本質は「打鍵をバイト列にして pty に流す」ことであり、テキストフィールドのような編集バッファを持ちません。カーソル位置も行の内容も、サーバー側のシェル(readline)が管理しています。
未確定領域を自前でバッファリングして描画する実装を入れていないアプリでは、次のいずれかが起きます。
- 確定前のひらがながそのままサーバーへ送られる ── 変換候補が出ないまま「にほんご」が行に入ります。
- Space が変換ではなく空白として届く ── コマンド行が壊れます。
- Enter が確定ではなく実行になる ── これが最も危険です。変換を確定したつもりで、書きかけのコマンドが実行されます。
- バックスペースが1バイト単位で効く ── UTF-8 の日本語は1文字3バイトなので、1回消すと不正なバイト列が残り、そこから表示が崩れます。
日本語ユーザーによる検証記事でも、Termius・ConnectBot・Termux について「変換候補が出ない」「Space を押すと確定してしまう」といった報告が繰り返し挙がっており、原因は一貫してターミナルエミュレータ側が marked text / composing text をハンドリングしていない点にあると整理されています(iruyo ブログの検証)。
解決できるのはアプリ開発者だけです。確定までは画面表示のみを更新し、確定した文字列だけをサーバーに送る独自の「IME モード」を実装する必要があります。ユーザー側の設定項目としては存在しません。
どのアプリでも使える3ステップの判定テスト
レビューを読むより、30秒で自分で確かめるほうが確実です。接続先で次を実行し、入力を安全に捨てられる状態を作ります。
cat > /tmp/ime-test.txt
この状態で「にほんご」と打ち、次の3点を見ます。
- 変換候補(または下線付きの未確定表示)が出るか。 出ないなら未確定文字列を扱っていません。
- Space を押したとき、候補が切り替わるか、空白が入るか。 空白が入るならアウトです。
- Enter を押したとき、確定だけで済むか、改行まで送られるか。 改行まで行くなら、実運用ではコマンドの誤爆リスクがあります。
最後に Ctrl-D で閉じて、バイト列を確認します。
od -An -tx1 /tmp/ime-test.txt
「日本語」と入力できていれば e6 97 a5 e6 9c ac e8 aa 9e が並ぶはずです。ここが化けていれば、そのアプリで日本語をターミナルに直接打つ運用は諦めたほうが早いです。
日本語入力の現実的な回避策
IME 対応が万全なモバイル SSH アプリは多くありません。実務では、日本語をターミナルに直接打たない設計にするのが正解です。効果の大きい順に並べます。
1. そもそも日本語が必要な場面を減らす。 サーバー運用のコマンドはほぼ ASCII です。日本語が要るのは実質、①日本語ログの grep パターン、②git commit のメッセージ、③設定ファイルの日本語コメント、の3つだけです。この3つを別ルートに逃がせば、日本語入力の問題は消滅します。
2. 別アプリで書いてペーストする。 メモアプリや検索欄で日本語を書き、コピーして貼り付けます。ペーストはブラケットペーストモードで1回のバイト列としてまとめて届くため、IME の未確定処理をまったく通りません。最も確実な方法です。
3. スニペットに登録する。 よく使う日本語の grep パターンや定型メッセージは、アプリのスニペット機能に入れておきます。1回登録すれば以後は入力不要になります。
4. grep のパターンをファイルに逃がす。 これはコマンドラインを ASCII だけで完結させられる手です。
# 検索語を書いたファイルを SFTP で置いておく
grep -f ~/patterns.txt /var/log/app.log
patterns.txt に「エラー」「タイムアウト」「失敗」を1行ずつ書いておけば、以後は ASCII だけで日本語ログを検索できます。
5. 日本語を含むファイルの編集は SFTP で往復させる。 サーバー上の vim で日本語を打とうとするのは、IME 問題とモーダルエディタの操作が二重にかかるので分が悪いです。SFTP で手元に落とし、スマホのエディタで編集して戻すほうが速く、事故も減ります(SSH でファイルを転送する)。
6. コミットメッセージは -m を避ける。 git commit -F message.txt の形なら、メッセージを別ルートで用意して渡せます。
7. AI に日本語で頼んでコマンドを書かせる。 アプリ内の AI 入力欄は普通の OS のテキストフィールドなので、IME は完全に動きます。日本語で意図を書いて、返ってくるのは ASCII のコマンドです ── ターミナル行に日本語を打つ必要そのものがなくなります。これは後の節で詳しく触れます。
ファイル名の文字化け:SFTP・macOS の NFD・git
表示が直っても、ファイル名だけ崩れることがあります。原因が3つに分かれるので、それぞれ別に扱います。
SFTP のファイル名にはエンコーディングの宣言がない
OpenSSH の sftp-server が話すのは SFTP プロトコルのバージョン3です。v3 ではファイル名は実質的に生のバイト列として流れ、どのエンコーディングかを宣言する仕組みがありません(UTF-8 を明示的に要求するのは v4 以降ですが、普及していません)。
結果として、クライアントは表示時にエンコーディングを推測します。サーバー上のファイル名が CP932 のまま作られていれば、UTF-8 前提のクライアントでは化けます。これはクライアントのバグではなく、プロトコルの制約です。
対処はサーバー側でファイル名を UTF-8 に直すことです。convmv が使えます。
sudo apt install -y convmv
convmv -f cp932 -t utf-8 -r --notest /srv/files
--notest を付けるまで実際には変更されないので、まず外して実行し、変換結果を確認してから付けてください。
macOS 由来の「か゛」問題は文字化けではない
macOS は HFS+ / APFS 上でファイル名を NFD(分解形) で保存します。「が」は「か」+ 結合文字の濁点(U+3099)という2文字として記録されます。Linux と Windows は NFC(合成形) が前提なので、Mac から転送したファイルは Linux 側で「か゛」のように見え、シェルの補完でも grep でも一致しません。
バイト列は正しく転送されているので、これは文字化けではなく正規化の不一致です。Linux 側で NFC に揃えれば解消します。
convmv -f utf-8 -t utf-8 --nfc -r --notest /srv/files
厄介なのは、Apple が macOS 10.12.6 / iOS 10.3.3 以降の APFS でランタイム正規化を導入したことです。Mac 上ではどちらの表記でも同じファイルが開けてしまうため、Mac 側では問題がまったく見えません。Linux に持っていった瞬間に発覚します。iPhone から Mac に SSH する 構成では、この非対称性を頭に入れておいてください。
Windows で作られた ZIP の中身
Windows のエクスプローラで作られた ZIP は、ファイル名が CP932 で格納されていることがあります。Linux でそのまま展開すると名前が化けます。Debian / Ubuntu の unzip にはエンコーディング指定オプションが入っています。
unzip -O cp932 archive.zip
git status の \346\227\245...
これは文字化けではなく、git の既定のクォート表示です。1行で消えます。
git config --global core.quotepath false
Shift_JIS / CP932 と Windows サーバー
日本の現場では、ログや CSV が Shift_JIS のまま残っているサーバーが普通にあります。ターミナルは UTF-8 のままにして、読むときだけ変換するのが正解です。
iconv -f CP932 -t UTF-8 /var/log/legacy/app.log | less
SHIFT_JIS ではなく CP932 を指定してください。 日本の業務システムのファイルはほぼ Windows 由来の CP932(Windows-31J)で、①、㈱、№ といった機種依存文字や、波ダッシュ(〜)の符号位置が標準の Shift_JIS と異なります。SHIFT_JIS を指定すると、そういう文字にぶつかった時点で iconv: illegal input sequence で停止します。
エンコーディングが不明なら nkf で判定できます。
sudo apt install -y nkf
nkf --guess file.csv # 判定だけ
nkf -w file.csv > out.csv # UTF-8 に変換
どうしても不正なバイトが残るときは iconv -c で捨てられますが、その文字は失われます。ログの調査で使うぶんには問題ありませんが、変換して保存する用途では使わないでください。
Windows サーバーに SSH したとき
Windows の OpenSSH サーバーに接続すると、セッションは cmd か PowerShell になり、表示はコンソールのコードページに従います。日本語版 Windows の既定は CP932 なので、UTF-8 前提のスマホ側ターミナルとは噛み合いません。
chcp 65001
PowerShell なら出力エンコーディングも合わせます。
[Console]::OutputEncoding = [Text.Encoding]::UTF8
注意点が1つあります。コードページの変更はそのコンソールセッション全体に影響します。 Windows 上で OpenSSH クライアントを実行するとコードページが 65001 に変わったまま戻らず、以後のコマンドが化けるという既知の挙動も報告されています(chcp 932 で戻せます)。Windows を踏み台にしている構成では、この副作用を踏まないよう ProxyJump で最終ホストまで一気に抜ける ほうが安全です。
TermAI での扱い
自社製品の話なので、事実と「自分で確認してほしいこと」を分けて書きます。
表示側について。 TermAI は iOS / Android の SSH・SFTP クライアントで、端末表示は UTF-8 で扱います。この記事の表示側の対処 ── サーバーの LANG を C.UTF-8 か ja_JP.UTF-8 にする、tmux を一度落とす、フォントを日本語対応のものにする ── は TermAI でもそのまま当てはまります。どのアプリを使っていても、サーバー側のロケール設定が最初にやるべきことです。
入力側について。 前節の3ステップテストは、TermAI でも実際に試して確認してください。IME の未確定文字列の扱いはアプリの実装依存で、バージョンによって変わる領域でもあります。「対応しています」と書くより、cat > /tmp/ime-test.txt して od で確かめてもらうほうが正確です。
日本語入力を迂回する設計。 TermAI の AI 入力欄は普通の OS のテキストフィールドなので、IME は完全に動きます。つまり、
「nginx のエラーログを直近10分だけ見たい」
と日本語で書けば、ASCII のコマンドが入力行に入ります。
journalctl -u nginx --since "10 min ago" -p err
ターミナル行に日本語を打つ必要そのものがなくなる、というのが実務上いちばん効く回避策です。日本語ログの grep パターンも同じ要領で生成できます。
AI は提案型です。 コマンドは入力行に書かれるだけで、実行するかどうかは人が判断します。本番サーバーで確認なしにコマンドが走る設計は選んでいません(AI のシェル提案をどこまで信じるか、AI にコマンドを書かせる)。無料枠は AI 提案が1日5回、SSH と SFTP は無制限で、内蔵の Tailscale を使えば 22番を公開しないまま自宅サーバーや VPS に到達できます。
鍵の保管まわりは SSH 秘密鍵をスマホに置くのは安全か、切り分け全般は SSH トラブルシューティング を参照してください。