標準出力にフィルタを挟むと子プロセスが TTY を失う
子プロセスの stdout にフィルタプロセスを差し込むと、子は `isatty()` が false のパイプを見る。TUI が壊れ、色が消え、Ansible のような TTY 判定を持つツールが挙動を変える。これは Unix の構造的制約で、1Password `op run` の透過マスクが踏んだ問題。回避は明示的なパイプ(`cmd | filter`)を既定にすること。
目次 5 sections
調査時点: 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 の制約。
回避策: 明示的なパイプを既定にする
<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 へ移植、概念ページとして再構成。冒頭に定義を追加