技術約5分で読めます

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)
Codexcodex-cli 0.150.1(Homebrewの @openai/codex、この日の昼に自動更新)
シェル・端末zsh 5.9、Ghostty(TERM=xterm-ghostty
stdio MCPbacklogdocker run -i)、qwen-mm-plugins-coreuvx)、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で止められる。
psT(stopped)、zshは suspended (tty input)
setsid の子は制御端末のないセッションに移り、/dev/tty を読んでもエラーで止まらない。
手元では、プロセスグループを分けるだけだと止まって、setsid まで通すと止まらなかった。

shellツールとMCPの起動

Codex 0.150.1のソースで、子プロセスの起動を並べるとこんな感じ。

子プロセスの起動端末の切り離し修正
shellツールのコマンド実行(core/src/spawn.rsdetach_from_tty()setsid)ありPR #9477、2026年1月19日
シェルスナップショット取得(shell_snapshot.rssetsid あり、stdinを /dev/nullPR #9477 と PR #9735(2026年1月30日)
exec-serverのpipe経由の子(utils/ptydetach_from_tty() ありPR #9477
ローカルstdio MCPサーバ(rmcp-client/src/stdio_server_launcher.rsprocess_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 runuvxpythoncode-mode-host)を止めたまま残していた。
backlogdocker 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)。
この場合は .zshrcbindkey -r '^T' を入れると起きなくなる、とコメントにある。