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.

Tired of Jira's slow UI? Drive it from your terminal with `jr`

If your day looks like open Jira → wait for the SPA to boot → hunt for the ticket → click into the status dropdown → wait again, you already know the tax. Most of what we actually do to a ticket — move it, comment on it, assign it — is two seconds of intent buried under ten seconds of web app. jira-sh is a tiny bash CLI that skips all of it. One command, jr, talking straight to the Jira Cloud REST API.

jr move PROJ-123 "In Review"
jr comment PROJ-123 "Deployed to staging"
jr view PROJ-123

No Electron, no browser, no waiting. It's a single bash script plus curl and python3 (standard library only for the core commands).

Setup in four lines

git clone https://github.com/shukebeta/jira-sh ~/Projects/jira-sh
bash ~/Projects/jira-sh/install.sh          # adds a source line to ~/.bashrc

# put these in ~/.bashrc
export JIRA_BASE=https://yourcompany.atlassian.net
export [email protected]
export JIRA_TOKEN=your-api-token

source ~/.bashrc
…more

IT blocked SSH to GitHub? One config line and you're done

If a network policy change suddenly breaks git push/pull to [email protected]:..., the panic response is for r in */; do git -C "$r" remote set-url origin ...; done across N repos. Don't. Add this to ~/.gitconfig and never think about it again:

[url "https://github.com/"]
    insteadOf = [email protected]:

What it does: any URL starting with [email protected]: is transparently rewritten to https://github.com/... at fetch/push time. git remote -v still shows the SSH form (good for humans), but the actual network request goes over HTTPS (good for the firewall).

Verify it works without pushing anything:

GIT_TRACE=1 git ls-remote [email protected]:YOUR_USER/YOUR_REPO.git

Look for run_command: ... remote-https ... in the output. If you see remote-ssh, the rule isn't matching (typo, wrong section, or a system-level gitconfig overriding it).

The rule also covers submodule URLs and any URL that happens to embed [email protected]: — they're all rewritten the same way.

If you have other GitHub Enterprise hosts ([email protected]:), add a matching pair for each. The pattern is always: target HTTPS host on the left, the SSH prefix you want rewritten on the right.

The one thing this doesn't do: change remote.origin.url in your .git/config. Existing repos still display the SSH form. If you want to be tidy, run a one-shot cleanup, but it's purely cosmetic — the wire traffic is already HTTPS.

Two GitHub accounts on one Windows box, HTTPS only? Don't let `gh` be your git credential helper

If you run gh auth setup-git, the GitHub CLI becomes git's credential helper — and it only ever hands out the token for the active account. The moment you git pull a repo belonging to your other account, you get the wrong token (403, or commits land under the wrong identity). There's no per-repo routing; you'd have to gh auth switch every single time you cross accounts.

You can prove it to yourself — ask the helper for the non-active account and it returns nothing:

"protocol=https`nhost=github.com`nusername=second-account`n`n" |
  & gh auth git-credential get      # exit 1, no token

The fix is to hand git over to Git Credential Manager (it already ships with Git for Windows). GCM picks the token by the username embedded in the remote URL, so routing becomes automatic. gh keeps working for gh issue/gh pr — it uses its own keyring, separate from the git helper.

Switch the helper and point GCM at GitHub:

git config --global --unset-all credential.https://github.com.helper
git config --global credential.helper manager
git config --global credential.https://github.com.provider github
git config --global credential.guiPrompt false   # terminal prompts, no GUI popup on a server
…more

tmux Alt+z for pane zoom works better with Chinese IME

tmux's built-in pane zoom (Prefix+z) can be unreliable for users with Chinese input methods. The standalone z keystroke may be intercepted by the IME before tmux sees it, forcing you to switch to English mode first.

Bind Alt+z as a prefix-free shortcut instead:

bind -n M-z resize-pane -Z

M-z (Alt+z) is sent as a Meta key sequence directly to the terminal, which most input methods don't intercept. The same logic applies to navigation—add hjkl pane switching without the prefix:

bind -n M-h select-pane -L
bind -n M-j select-pane -D
bind -n M-k select-pane -U
bind -n M-l select-pane -R

This keeps tmus responsive regardless of your current input state.