OSC 3008 とは:systemd の端末コンテキスト信号と文字化けの対処

UAPI.15 OSC 3008 Hierarchical Context Signalling の役割、送信されるデータ、systemd での使われ方、古い端末や踏み台、非対応環境で文字化けが出るときの確認と無効化方法を説明する。

OSC 3008、正式には Hierarchical Context Signalling は、UAPI Group の仕様 UAPI.15 です。端末セッション内の現在の「コンテキスト階層」を、プログラムが端末エミュレーターへ伝えるための新しい端末制御シーケンスを定義しています。

簡単に言うと、現代の Linux 端末でよく出てくる「いま自分はどの階層にいるのか」という問題を解決しようとするものです。

たとえば、ローカルマシンからサーバーへ SSH し、さらにコンテナへ入り、その後 run0 や類似ツールで権限昇格したコマンドを実行することがあります。ユーザーから見ると 1 つの連続した端末ウィンドウですが、端末エミュレーターから見ると、追加の信号がなければ現在の出力がローカル、リモート、コンテナ、権限昇格後のコマンドのどれに属するのかを正確に知るのは難しいです。

OSC 3008 はこのような場面のために設計されています。UAPI の公式仕様では、端末エミュレーターが画面上の現在内容のコンテキスト階層を追跡できると説明されています。systemd も対応実装を提供しており、通常は 80-systemd-osc-context.sh というスクリプト名で使われます。

公式仕様:

OSC 3008 が必要な理由

従来の端末は基本的にプログラムの出力を表示するだけです。せいぜいウィンドウタイトル、色、プロンプトで環境の違いを補助する程度でした。しかし、現代の端末セッションは多層にネストされがちです。

1
local shell -> SSH remote server -> Docker/Podman/systemd-nspawn container -> run0 elevated command

PS1 だけに頼ると、いくつか問題が出ます。

  1. プロンプトは現在の shell しか表しにくく、ネスト階層全体を表現しづらい。
  2. shell、ディストリビューション、ユーザー設定によって互いに上書きされる。
  3. 端末エミュレーターが、どの出力がどのコンテキストに属するかを確実に判断できない。
  4. 一部の制御シーケンスが非対応端末でそのまま文字化けとして表示される。

OSC 3008 は、より構造化された方法を提供します。コンテキストへ入るプログラムが start シーケンスを送り、コンテキストから出るプログラムが end シーケンスを送ります。端末エミュレーターはそれを解析し、現在の出力がどのコンテキストノードに属するかを把握できます。

どんな情報を送るのか

OSC 3008 は OSC エスケープシーケンスの集合です。仕様では、次の 2 種類の中核コマンドが定義されています。

1
2
OSC "3008;start=..." ST
OSC "3008;end=..." ST

start はコンテキストの開始、更新、または再び現在のコンテキストになったことを表します。end はコンテキストの終了を表します。

各コンテキストにはメタデータフィールドを付けられます。よく使われるフィールドは次のとおりです。

  1. type=:コンテキスト種別。例:shellcommandremotecontainervmelevate
  2. user=:シーケンスを送った UNIX ユーザー名。
  3. hostname=:ホスト名。
  4. machineid=/etc/machine-id 由来の machine ID。
  5. bootid=/proc/sys/kernel/random/boot_id 由来の boot ID。
  6. pid=:シーケンスを送ったプロセスの PID。
  7. comm=:プロセス名。
  8. cwd=:現在の作業ディレクトリ。主に shellcommand 用。
  9. cmdline=:対話的に呼び出されたコマンドライン。
  10. container=vm=:コンテナ名または仮想マシン名。
  11. targetuser=targethost=:対象ユーザーまたはリモート対象ホスト。

これらのフィールドはユーザーが直接読むためのものではなく、端末エミュレーターが解析するためのものです。対応端末はこれらの制御シーケンスを消費し、UI 体験の改善に使います。

何が便利になるのか

端末エミュレーターが OSC 3008 に対応していると、より賢い表示ができます。

第一に、異なるコンテキストの出力を区別できます。たとえばコンテナ内の出力、権限昇格コマンドの出力、リモート SSH の出力に、別の背景、枠、ヒントを付けられます。

第二に、階層のパンくず表示ができます。端末は現在の状態をおおよそ次のように把握できます。

1
local machine -> remote host -> container -> root command

これはウィンドウタイトルや PS1 だけに頼るより安定しています。

第三に、ウィンドウタイトル、タブ、コンテキストメニューの補助にも使えます。現在のコンテキストに応じてタブタイトルを更新したり、特定の出力に対して「同じディレクトリで shell を開く」といった操作を提供できます。

第四に、誤操作を減らせます。たとえば本番環境のコンテナ内で root コマンドを実行している場合、端末がより目立つ警告を出し、危険なコマンドを間違った環境で実行する可能性を下げられます。

systemd はどう組み込んでいるのか

systemd の実装は主に 80-systemd-osc-context.sh にあります。このスクリプトは profile の仕組みで読み込まれ、対話的 Bash 環境で関連関数とプロンプトフックを設定します。

よくあるパスは次のようなものです。

1
2
/etc/profile.d/80-systemd-osc-context.sh
/usr/lib/systemd/profile.d/80-systemd-osc-context.sh

実際の場所はディストリビューションによって異なります。systemd のソース中のコメントでは、このファイルは systemd-tmpfiles によって有効化され、/etc/profile.d/ へリンクされると説明されています。

Bash では、次のような関数名が使われます。

1
2
3
__systemd_osc_context_escape
__systemd_osc_context_common
__systemd_osc_context_precmdline

同時に PROMPT_COMMANDPS0 にも影響します。PS0 は Bash がコマンドを読み取った後、実行する直前に展開されます。そのため、非対応端末では各コマンド実行前に長い 3008;start=... テキストが表示されることがあります。

なぜ一部の端末では文字化けするのか

通常、端末エミュレーターは OSC 3008 を制御シーケンスとして解析し、直接表示しません。

しかし端末が OSC 3008 を認識しない場合、または中間層が制御文字をフィルタ、エスケープ、分断した場合、元の内容がそのまま表示されることがあります。よくある環境は次のとおりです。

  1. 古い端末エミュレーター。
  2. OSC 3008 に未対応の SSH クライアント。
  3. Web 踏み台やブラウザー端末。Apache Guacamole 環境など。
  4. Emacs term.el、シリアル端末、minicom など OSC 対応が限定的な環境。
  5. 多段転送で中間層が制御シーケンスを壊す場合。

このとき、次のような内容が表示されることがあります。

1
]3008;start=...;type=command;cwd=...

また、machineid=bootid=pid=comm=cwd= を含む長い文字列が見えることもあります。これは通常のプログラム出力ではなく、本来は端末が処理するべきコンテキスト信号です。

OSC 3008 かどうかを判断する方法

まず 3 つの手がかりを見ます。

第一に、文字化けの中に 3008;start= または 3008;end= があるか。

第二に、コマンド実行の前後、特に Enter でコマンドを実行するたびに出るか。

第三に、現在のシステムが systemd の OSC 関数を読み込んでいるか。

1
declare -f __systemd_osc_context_precmdline

関数定義が表示されれば、現在の shell に関連ロジックが読み込まれています。

profile ファイルも確認できます。

1
ls -l /etc/profile.d/80-systemd-osc-context.sh

このパスが存在し、かつ端末が OSC 3008 に対応していない場合、文字化けの原因はここである可能性が高いです。

一時的な回避策

現在のセッションだけで見た目を直したい場合は、関連関数を上書きして PS0 を空にできます。

1
2
3
4
__systemd_osc_context_precmdline() { :; }
__systemd_osc_context_common() { :; }
__systemd_osc_context_escape() { :; }
PS0=""

SSH セッションだけで自動処理したい場合は、~/.bashrc に入れます。

1
2
3
4
5
6
7
8
if [[ -n "$SSH_CLIENT" || -n "$SSH_TTY" || -n "$SSH_CONNECTION" ]]; then
    if declare -f __systemd_osc_context_precmdline >/dev/null; then
        __systemd_osc_context_precmdline() { :; }
        __systemd_osc_context_common() { :; }
        __systemd_osc_context_escape() { :; }
        PS0=""
    fi
fi

この方法は影響範囲が小さく、システム全体の profile ファイルを変更せず、後で削除するのも簡単です。

システム全体で無効化する方法

このマシンの端末環境が全体的に OSC 3008 と相性が悪いと確認できている場合は、システム全体で無効化することも考えられます。ただし、すべてのユーザーとログインセッションに影響するため慎重に行ってください。

systemd スクリプトのコメントでは、/etc/profile.d/80-systemd-osc-context.sh のシンボリックリンクを削除し、対応する tmpfiles 断片を mask して再作成を防ぐという考え方が示されています。

次のようなコマンドを参考にできます。

1
2
3
test -h /etc/profile.d/80-systemd-osc-context.sh && \
rm -v /etc/profile.d/80-systemd-osc-context.sh && \
ln -s /dev/null /etc/tmpfiles.d/20-systemd-osc-context.conf

これは ~/.bashrc だけを変更するより重い対応です。まず一時回避策で問題が OSC 3008 によるものか確認してから、システム全体で無効化するか判断してください。

個人の SSH クライアントだけが非対応なら、いきなりシステム設定を変えるのはおすすめしません。自分の ~/.bashrc を変更するか、SSH クライアントのログイン後コマンドでクリーンアップするほうが安全です。

端末をアップグレードするか、OSC 3008 を無効化するか

使っている端末がすでに OSC 3008 に対応しているなら、この機能は残すほうがよいです。将来的にリモート shell、コンテナ、仮想マシン、権限昇格コマンドのコンテキスト表示をよりわかりやすくしてくれる可能性があります。

一方、実際の環境が踏み台、シリアル、古い端末、Web SSH で、短期的にクライアントを更新できないなら、抑止するほうが現実的です。多くの運用場面では、拡張コンテキスト表示よりも、きれいで読める端末出力のほうが重要です。

次の順番で考えるとよいです。

  1. 端末エミュレーターをアップグレードできるなら、まずアップグレードする。
  2. アップグレードできず、自分だけに影響するなら、~/.bashrc またはクライアントのログイン後コマンドで抑止する。
  3. マシン全体が影響を受けるなら、/etc/profile.d/80-systemd-osc-context.sh を削除し、tmpfiles 断片を mask することを検討する。

短いまとめ

OSC 3008 はマルウェアでも、通常のプログラムが勝手に出している文字列でもありません。UAPI.15 で定義された端末コンテキスト信号であり、shell、SSH、コンテナ、仮想マシン、権限昇格コマンドの階層関係を端末に理解させることが目的です。

問題は多くの場合、互換性です。対応端末は自動的に解析します。非対応端末では、制御シーケンスが文字化けとして表示されることがあります。

3008;start=machineid=bootid=cwd= のような内容が見えたら、まず systemd の 80-systemd-osc-context.sh によって注入されているか確認してください。そのうえで、影響範囲に応じて一時回避、個人の .bashrc での抑止、またはシステム全体での無効化を選びます。

SSH で OSC 3008 のエスケープシーケンスを抑える方法

SSH で Ubuntu、Kubuntu、または比較的新しい systemd ベースの Linux システムへ接続すると、コマンドプロンプト付近に奇妙な制御文字が表示されることがあります。手がかりになる文字列は OSC 3008、systemd context、__systemd_osc_context_* などです。通常はコマンド実行には影響しませんが、端末表示が汚れ、ログをコピーするときに余計な文字列が混ざります。

この問題は、特定の SSH クライアントだけに限定されるものではありません。WindTerm、組み込み端末、古い端末エミュレーター、OSC シーケンスの対応が不完全なクライアントでは、本来端末が解釈すべき制御シーケンスがそのまま表示されることがあります。

よくある原因は、リモートシステムが Bash 環境で systemd の OSC コンテキストフックを読み込んでいる一方、現在の SSH クライアントがそれらのシーケンスを正しく処理できていないことです。対処の考え方は単純です。フック関数が存在することを確認したうえで、それらを空の関数に上書きし、PS0 を空にします。

以下では 2 つの方法を紹介します。1 つ目はリモートの ~/.bashrc に書く方法で、SSH セッション全体を一括で処理したいサーバーに向いています。2 つ目は SSH クライアントの「ログイン後に実行するコマンド」に書く方法で、特定のクライアントやセッションだけに効かせたい場合に向いています。

方法 1:.bashrc で SSH セッションを判定して抑止する

この方法のロジックは、現在のセッションが SSH リモートセッションであり、systemd が注入した OSC フック関数が実際に存在する場合だけ、関連関数を空関数に上書きし、PS0 を空にするというものです。

次のブロックをリモートサーバーの ~/.bashrc の末尾に追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# SSH 接続を検出したとき、systemd OSC 3008 などの端末制御シーケンスの文字化けを抑止する
if [[ -n "$SSH_CLIENT" || -n "$SSH_TTY" || -n "$SSH_CONNECTION" ]]; then
    # systemd の OSC 関数が実際に読み込まれている場合だけ上書きする
    if declare -f __systemd_osc_context_precmdline >/dev/null; then
        __systemd_osc_context_precmdline() { :; }
        __systemd_osc_context_common() { :; }
        __systemd_osc_context_escape() { :; }
        PS0=""
    fi
fi

保存したら SSH にログインし直すか、現在の shell で次を実行します。

1
source ~/.bashrc

文字化けの原因が systemd の OSC フックであれば、新しい shell に入った後は通常表示されなくなります。

特定クライアントにも対応したい場合

一部のクライアントは独自の環境変数を設定します。たとえば WindTerm では TERM_PROGRAM=WindTerm が設定される場合があります。この判定も残したい場合は、少し広めに次のように書けます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# WindTerm または SSH 接続を検出したとき、systemd OSC 3008 などの端末制御シーケンスの文字化けを抑止する
if [[ "$TERM_PROGRAM" == "WindTerm" || -n "$SSH_CLIENT" || -n "$SSH_TTY" || -n "$SSH_CONNECTION" ]]; then
    # systemd の OSC 関数が実際に読み込まれている場合だけ上書きする
    if declare -f __systemd_osc_context_precmdline >/dev/null; then
        __systemd_osc_context_precmdline() { :; }
        __systemd_osc_context_common() { :; }
        __systemd_osc_context_escape() { :; }
        PS0=""
    fi
fi

通常の SSH では、SSH_CLIENTSSH_TTYSSH_CONNECTION の判定だけで十分です。TERM_PROGRAM を加えるのは、一部クライアントの識別方法も拾うためです。

この書き方が比較的安全な理由

この設定には 2 つの制限があります。

1 つ目はセッション判定です。

1
[[ -n "$SSH_CLIENT" || -n "$SSH_TTY" || -n "$SSH_CONNECTION" ]]

これらの変数は通常 SSH ログインセッションでだけ現れるため、主にリモートログインに作用し、ローカルのデスクトップ端末を不用意に変えることはありません。

2 つ目は関数の存在確認です。

1
declare -f __systemd_osc_context_precmdline >/dev/null

これは安全弁です。現在の shell が __systemd_osc_context_precmdline をすでに読み込んでいる場合だけ、関連関数を上書きします。systemd OSC の仕組みがない環境では、この設定は余計な処理をしません。

実際に上書きするのは次の関数です。

1
2
3
4
__systemd_osc_context_precmdline() { :; }
__systemd_osc_context_common() { :; }
__systemd_osc_context_escape() { :; }
PS0=""

これにより、関連フックが内容を出力しなくなります。PS0 は Bash がコマンドを読み取った後、実行する直前に展開するプロンプト変数です。一部の OSC シーケンスは PS0 や類似の仕組みを通じて挿入されるため、ここで一緒に空にします。

方法 2:SSH クライアントのログイン後コマンドに入れる

リモートサーバーの ~/.bashrc を変更したくない場合は、修正コマンドを SSH クライアントの「ログイン後に実行するコマンド」に入れます。多くの端末や SSH クライアントには似た機能があり、名前は次のような場合があります。

  1. Command executed after authentication
  2. Post-login command
  3. Remote command after login
  4. ログイン後に実行するコマンド

次の 1 行を設定します。

1
__systemd_osc_context_precmdline() { :; }; __systemd_osc_context_common() { :; }; __systemd_osc_context_escape() { :; }; PS0=""

クライアントが自動実行後に新しいプロンプト行へ移るための改行を要求する場合は、そのクライアントの構文に従って改行を追加します。たとえば WindTerm の Command executed after authentication では次のようにできます。

1
__systemd_osc_context_precmdline() { :; }; __systemd_osc_context_common() { :; }; __systemd_osc_context_escape() { :; }; PS0="" \n \n

2 つの \n は、クライアントにクリーンアップコマンドを実行させ、その後新しいプロンプト行へ移らせるためのものです。この書き方は、認証後の自動コマンドに対応する WindTerm のようなクライアントに向いており、Kubuntu 24.04 環境で動作確認されています。

この方法は影響範囲が小さいのが利点です。そのクライアント、そのセッションで接続した場合だけクリーンアップが実行され、サーバー側の shell 設定を変更する必要がありません。

どちらを選ぶか

そのサーバーを主に自分だけが使っている、またはすべての SSH クライアントでこの文字化けを避けたいなら、方法 1 がおすすめです。~/.bashrc に書いておけば、WindTerm、Windows Terminal、Tabby、Xshell、その他の SSH クライアントでも同じように処理できます。

共有サーバーである、または特定のクライアントだけで回避したいなら、方法 2 がおすすめです。リモート設定を変更せず、他のユーザーの shell 環境にも影響しません。

まず方法 2 で問題が解決することを確認してから、方法 1 を ~/.bashrc に入れるか決めるのもよいです。

注意点

どちらの方法も systemd の OSC 出力フックを抑止するだけです。systemd サービスを変更するわけではなく、SSH ログイン自体にも影響しません。

ただし、OSC 3008 に対応したモダンな端末を使っていて、コマンドコンテキスト、作業ディレクトリ、システム状態の表示に依存している場合は、抑止後にそれらの拡張表示が消える可能性があります。通常の SSH 運用、開発機へのログイン、サーバー管理では大きな問題にならないことが多いです。

また、最初からグローバルな /etc/bash.bashrc にこの上書きを書くのはおすすめしません。マシン上の全ユーザーに必要だと確認できている場合を除き、個人環境では ~/.bashrc、特定クライアント向けには SSH クライアントのログイン後コマンドを優先してください。

短いまとめ

SSH で Linux に接続したあと systemd OSC 3008 の文字化けが出る場合は、次の順番で対応できます。

  1. サーバーを変更したくない場合: SSH クライアントにログイン後のクリーンアップコマンドを設定する。
  2. 自分のサーバーの場合: SSH セッション判定のブロックを ~/.bashrc に入れる。
  3. 共有サーバーの場合: まずクライアント側の方法で確認し、その後 shell 設定へ広げるか判断する。

要点は、SSH セッション内だけで systemd の OSC フック関数を確認し、存在する場合は空関数に上書きして PS0 を空にすることです。これで文字化けを抑えつつ、通常の端末環境への影響をできるだけ小さくできます。