Codexが suspended (tty input) で止まる原因をMac miniで調べた
目次
Codexを起動して少し使うと、zsh: suspended (tty input) でシェルに戻ることがこのところ続いた。
昨日は goal コマンドで朝までやっといたのが、同じエラーで止まっていた。
正直、goal の意味がない。
--dangerously-bypass-approvals-and-sandbox で動かしていたのでフラグをまず疑って、M4 Mac miniで再現とソースと実機のプロセスツリーを探ってみた。
検証環境
| 項目 | 内容 |
|---|---|
| マシン | M4 Mac mini(macOS 15.5、Darwin 25.5.0、arm64) |
| Codex | codex-cli 0.150.1(Homebrewの @openai/codex、この日の昼に自動更新) |
| シェル・端末 | zsh 5.9、Ghostty(TERM=xterm-ghostty) |
| stdio MCP | backlog(docker run -i)、qwen-mm-plugins-core(uvx)、codex_apps、内部の code-mode-host |
ptyでの再現
suspended (tty input) は、端末のフォアグラウンドではないプロセスグループが端末入力を読もうとして、SIGTTINで止められた状態だ(終了コードは 128 + 21 = 149)。
素のCodexでは条件が絞りにくいので、pty(擬似端末)の上で小さく再現してみた。
TUI役のプロセスが下の3通りで子プロセスを起動し、/dev/tty を読ませる仕組みにした。
| 子プロセスの起動 | 制御端末 | 子が端末を読むと |
|---|---|---|
| 親と同じプロセスグループ | 共有 | 読める(停止しない) |
独自のプロセスグループ(setpgid) | 共有したまま | SIGTTINで停止(ps の状態が T) |
新セッション(setsid) | 切り離される | 端末を持たず、読み取りは失敗する(停止しない) |
止まったのは真ん中だけ。
独自のプロセスグループに入った子は端末のフォアグラウンドから外れる。
制御端末は親と共有したままなので、/dev/tty を読むとSIGTTINで止められる。
ps は T(stopped)、zshは suspended (tty input)。
setsid の子は制御端末のないセッションに移り、/dev/tty を読んでもエラーで止まらない。
手元では、プロセスグループを分けるだけだと止まって、setsid まで通すと止まらなかった。
shellツールとMCPの起動
Codex 0.150.1のソースで、子プロセスの起動を並べるとこんな感じ。
| 子プロセスの起動 | 端末の切り離し | 修正 |
|---|---|---|
shellツールのコマンド実行(core/src/spawn.rs) | detach_from_tty()(setsid)あり | PR #9477、2026年1月19日 |
シェルスナップショット取得(shell_snapshot.rs) | setsid あり、stdinを /dev/null に | PR #9477 と PR #9735(2026年1月30日) |
exec-serverのpipe経由の子(utils/pty) | detach_from_tty() あり | PR #9477 |
ローカルstdio MCPサーバ(rmcp-client/src/stdio_server_launcher.rs) | process_group(0) のみ | 未修正 |
detach_from_tty() は setsid() で、EPERM のときだけ setpgid に切り替わる。
最初の修正は継承stdioの子で setpgid をやめたPR #8691(2026年1月8日、Elixirのコマンドが同じSIGTTINでハングした不具合)だ。
続く#9477がshellツールとスナップショットに setsid を足し、#9735がスナップショットのstdinを /dev/null にした。
いずれも2026年1月にマージされている。
ローカルのstdio MCPサーバを起動する箇所だけは、command.process_group(0) で独自プロセスグループに移すだけで、セッションは切り離していない。
MCPの旧プロトコルと新プロトコル、どちらの分岐でも同じで、pty再現の真ん中の条件がそのまま残っている。
この停止は、Issueとして未解決のまま残っている(openai/codex #18656 Stdio MCP servers can trigger terminal job-control suspension during startup on macOS、bug・mcpラベル、open)。
報告者は、子を別プロセスグループに移しても制御端末は共有したままで、そのツリーの何かが端末を読み書きするとzshに止められる、と書いている。
setsid() か TIOCNOTTY で切り離す修正を提案している。
手元のMCPのプロセスツリー
MCPサーバ本体はstdin・stdoutがパイプなので端末を使わない。
端末を使うとしたら子孫で、backlog は次のように子を起動する。
codex(TUI) ── docker run -i ── (backlog-mcp-serverコンテナ)
└─ uvx ── git / python (qwen-mm-plugins-coreの取得)
└─ code-mode-host
ps で確認すると、MCPの子はどれも起動したターミナル ttys025 を持ち、プロセスグループはCodexと別だった。
このツリーに /dev/tty を読むものがあれば、そこでSIGTTINが飛ぶ。
どのサーバのどの呼び出しがそれをやっているかは、1つずつ止めて確かめる必要があって、今回はそこまでやっていない。
Issue #18656のほうも、特定のサーバまでは絞っていない。
同じマシンで -s read-only(サンドボックスあり)のまま常駐している別のCodexでも、npx で起動した backlog の子は独自プロセスグループのまま、ttys001 をCodexと共有していた。
MCPを起動する stdio_server_launcher.rs はサンドボックスも承認の設定も受け取らないので、バイパスのフラグでは変わらない。
Issue #18656の再現手順も「普通に起動」で、バイパスは出てこない。
残った98個のコンテナ
止まったCodexは後始末を実行できないので、MCPの子プロセスが動いたまま残る。
実機で T(停止)のCodexが2つ見つかった。
$ ps -axo pid,pgid,stat,tty,command | awk '$3 ~ /T/'
6185 6185 T ttys004 node .../codex --dangerously-bypass-approvals-and-sandbox
17900 17900 T ttys030 node .../codex --dangerously-bypass-approvals-and-sandbox
8月25日に起動したものと、この日に起動したものだ。
どちらもMCPのサブツリー(docker run・uvx・python・code-mode-host)を止めたまま残していた。
backlog は docker run --rm で起動するので、コンテナが終了すれば自動で消える。
ところがCodexが止まるとMCPを畳めず、docker run もコンテナも動いたままになり、--rm の削除が起きない。
backlog-mcp-server のコンテナが98個、合計およそ6.2GiB分たまっていた。
最古は8月24日で、Docker Desktopの割当8GiBがほぼ埋まっていた。
$ docker ps --filter ancestor=ghcr.io/nulab/backlog-mcp-server -q | wc -l
98
98個はこの環境での実測で、全てのCodexが必ずこうなるわけではないと思う。
止まったCodexが増えるほど、解放されないプロセスが積み重なっていく。
回避策
止まった直後は jobs -l で番号を確認し、fg で戻せば動き出す。
停止が解けるだけで、また同じ条件で止まる。
頻度を下げるなら、stdio MCPサーバを1つずつ外して、どれが原因か確かめる。
Issue #9493のコメントには、Codexをラップして trap '' TTIN でSIGTTINを無視させるユーザー側の緩和も出ているが、停止を防ぐだけで直るわけではない。
古いバージョン(0.46.0)では、プロンプト入力中のCtrl+Tで同じ停止に入る不具合も報告されている(Issue #5195、closed)。
この場合は .zshrc に bindkey -r '^T' を入れると起きなくなる、とコメントにある。