SSH 終端出現 systemd OSC 3008 亂碼:兩種簡單處理辦法

整理 SSH 連接 Ubuntu 等 Linux 系統時出現 systemd OSC 3008 亂碼的處理方案:可在 .bashrc 中按遠端會話攔截,也可在支援登入後命令的 SSH 客戶端裡定向清理。

用 SSH 連接 Ubuntu、Kubuntu 或其他 systemd 較新的 Linux 系統時,有時會在命令提示字元附近看到一串奇怪的控制字元,常見關鍵字包括 OSC 3008、systemd context、__systemd_osc_context_*。它通常不影響命令執行,但會污染終端顯示,複製日誌時也容易夾帶亂碼。

這個問題不一定只和某一個 SSH 客戶端有關。WindTerm、某些嵌入式終端、舊版終端模擬器,或者對 OSC 序列支援不完整的客戶端,都可能把本該被終端解讀的控制序列直接顯示出來。

根因通常是:遠端系統在 Bash 環境裡載入了 systemd 的 OSC 上下文鉤子,而目前 SSH 客戶端沒有正確處理這些序列。處理思路也很直接:確認這些鉤子函數存在後,把它們覆蓋成空函數,並清空 PS0

下面給兩個辦法。第一個寫在遠端 ~/.bashrc,適合想一勞永逸處理 SSH 會話的伺服器;第二個寫在 SSH 客戶端的「登入後執行命令」裡,適合只想對某個客戶端或某個會話生效的場景。

方案一:在 .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 只是為了覆蓋某些客戶端自己的識別方式。

為什麼這個寫法相對安全

這段配置有兩層限制。

第一層是會話判斷:

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

這些變數通常只在 SSH 登入會話裡出現,所以它主要影響遠端登入,不會隨便改動本機桌面終端。

第二層是函數存在性判斷:

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 序列正是透過它或類似機制插進去的,所以這裡也一起清空。

方案二:放到 SSH 客戶端的登入後命令裡

如果你不想改遠端伺服器的 ~/.bashrc,可以把修復動作放到 SSH 客戶端的「登入後執行命令」裡。很多終端或 SSH 客戶端都有類似功能,名字可能叫:

  1. Command executed after authentication
  2. Post-login command
  3. Remote command after login
  4. 登入後執行命令

填入下面這一行:

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

這裡的兩個 \n 用來讓客戶端執行清理命令後再進入新的提示行。這個寫法適合 WindTerm 這類支援認證後自動執行命令的客戶端,在 Kubuntu 24.04 環境裡測試可用。

這種方案的好處是影響範圍小:只有使用該客戶端、該會話連接時才會執行清理命令,伺服器上的 shell 配置不需要改。

兩種方案怎麼選

如果這台伺服器主要是你自己用,或者你希望所有 SSH 客戶端連進來都不再看到這類亂碼,推薦用方案一。寫進 ~/.bashrc 後,換 WindTerm、Windows Terminal、Tabby、Xshell 或其他 SSH 客戶端,都能統一處理。

如果伺服器是多人共用,或者你只想讓某個客戶端規避這個問題,推薦方案二。它不改遠端配置,也不會影響其他人的 shell 環境。

也可以先用方案二驗證問題確實能解決,再決定要不要把方案一寫進伺服器的 ~/.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。這樣能壓住亂碼,又盡量不影響正常終端環境。

记录并分享
使用 Hugo 建立
主題 StackJimmy 設計