標準出力にフィルタを挟むと子プロセスが TTY を失う

一言まとめ

子プロセスの stdout にフィルタプロセスを差し込むと、子は `isatty()` が false のパイプを見る。TUI が壊れ、色が消え、Ansible のような TTY 判定を持つツールが挙動を変える。これは Unix の構造的制約で、1Password `op run` の透過マスクが踏んだ問題。回避は明示的なパイプ(`cmd | filter`)を既定にすること。

作成 · 更新 · #unix#tty#pipe#shell
目次 5 sections
  1. 01 何が起きるか
  2. 02 回避策: 明示的なパイプを既定にする
  3. 03 採らなかった自動化の手段
  4. 04 関連
  5. 05 更新履歴

調査時点: 2026-04。公開 OSS の mask-pipe(github.com/c12o-dev/mask-pipe)がシェル統合の既定を決めたときの ADR から。

TTY は、プログラムが入出力先を対話用の端末として扱うための仕組み。TUI は、その端末上に文字で画面や操作部品を描くユーザーインターフェースを指す。

何が起きるか#

「.zshrc に 1 行足せば全部の出力が自動でマスクされる」という透過型の設計は、子プロセスの stdout をフィルタプロセスのパイプに繋ぎ替える。すると子プロセスから見た stdout は端末ではなくパイプになり、isatty() が false を返す。

1Password の op run がこれをやった結果、次が報告されている。

  • TUI アプリケーションが壊れる(charmbracelet/crush の例)
  • ls / grep / git の色付き出力が消える(TTY でないと色を出さない既定のため)
  • Ansible 統合が壊れる(TTY 判定で挙動を変えるため)
  • 唯一の回避策が --no-masking で、マスクの目的自体が消える

実装バグではなく、パイプラインの途中にプロセスを挟む以上避けられない Unix の制約。

回避策: 明示的なパイプを既定にする#

Terminal window
<command> | mask-pipe

利用者が意識してパイプを繋ぐ形を主経路にすると、TUI・対話プロンプト・色付き出力を壊さず、いつマスクが掛かっているかが見える。既定のインストールがシェルの挙動に一切影響しない。

非対話コマンド向けのシェル関数ラッパー(mlogs() { docker logs "$@" | mask-pipe; })はオプトインの便宜として文書化し、対話・TUI コマンドを包まないよう注意書きを付ける。

代償は、毎回 | mask-pipe を打つ手間と、「設定して忘れたい」需要に応えられないこと。応えるなら PTY プロキシ(mask-pipe shell のような形)が要り、実装は重い。

採らなかった自動化の手段#

手段棄却理由
LD_PRELOAD で write() を横取り静的リンクの Go バイナリ(docker、kubectl)は libc を経由しない。セキュリティ重視の環境では LD_PRELOAD が無効化されている。マルチスレッドの正しさが難しい
zsh の preexec フックでコマンドを書き換えサブシェル・exec・絶対パス指定で素通りする。`exec
unbuffer / script で包む包んだコマンドには TTY が残るが、その出力を別フィルタに通す下流で TTY が失われる
共通コマンド用ラッパーを init で一括導入部分解。README に例を置くに留め、同梱はしない

関連#

更新履歴#

  • 2026-04-16: 初版(mask-pipe ADR-0002)
  • 2026-10-17: Wiki へ移植、概念ページとして再構成。冒頭に定義を追加

出典

  1. https://www.1password.community/discussions/developers/op-run-changes-stdout-and-stderr-to-not-be-ttys-when-masking/26040
  2. https://github.com/charmbracelet/crush/issues/1415

同じトピック — CLI