Native TUI hangs in tmux on Git Bash? It's winpty, pcon, and fork-vs-exec

Launching a native Windows console TUI (Claude Code, codex, etc.) from a tmux pane on Git Bash gives you a frozen screen with a blinking cursor. The usual advice is "wrap it in winpty" — that's wrong, and it took three layers to get to the real fix.

winpty cannot bridge through tmux

winpty <app> works fine in plain mintty, so it's tempting. But inside a tmux pane it deadlocks: as the pane's initial command it hangs forever; as a child of the pane shell it exits with stdin is not a tty. winpty needs to own a Windows console and bridge stdio, and tmux's pty layer breaks that bridge. Don't reach for it in tmux.

The console comes from ConPTY, which disable_pcon kills

Native Windows apps can't use an MSYS/Cygwin pty as a console — to them it's just a pipe, so isatty() is false and the TUI renders garbled or bails. Modern MSYS solves this with ConPTY backing, controlled by the MSYS env var. Check yours:

echo "$MSYS"
# winsymlinks:nativestrict disable_pcon   <- the killer

disable_pcon (often set in dotfiles to mute some harmless MSYS warnings) turns off exactly the ConPTY backing the native app needs. Flip it back on, scoped to just the launch so your interactive shell keeps its setting:

MSYS=enable_pcon claude   # renders correctly

...but only if the app is forked, not exec'd

Here's the part that cost the most time. MSYS=enable_pcon claude works when you type it by hand, yet the same command wired into a launcher script still hangs. The difference is fork vs exec.

Cygwin only allocates a ConPTY when it spawns (fork+exec) a native console app. An interactive shell forks every command, so hand-typing works. A launcher built as an all-exec chain —

bash -lc 'exec bash launch.sh'  ->  exec ... ->  exec env ... claude

— never forks: the native binary replaces the pane-leader process in place via execve, Cygwin never runs its spawn path, no ConPTY, dead TUI. Even a non-interactive bash -c '... claude' works, because that forks.

The fix is to stop exec'ing into the agent and let the shell fork it as a child:

# was:  exec env MSYS=enable_pcon ... claude "$@"
env MSYS=enable_pcon ... claude "$@"
exit $?

The trailing exit $? matters twice: it propagates the child's status so the pane still closes when the app quits, and it defeats bash's "last command gets exec'd, not forked" optimization — guaranteeing the fork. The child stays in the pane's foreground process group, so the pty hangup on pane close still reaches it; nothing gets orphaned.

So the full recipe for a native console TUI in tmux on Git Bash: skip winpty, scope MSYS=enable_pcon to the launch, and make sure something forks the binary instead of exec'ing into it.

Comments

  1. Markdown is allowed. HTML tags allowed: <strong>, <em>, <blockquote>, <code>, <pre>, <a>.