Posts tagged with “tools”

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.

Garbled text before PS1 over SSH? Your OSC sequences are leaking

You SSH into a machine and see garbage like ile://host/home/userurrentDir=/home/user before your prompt. Locally everything is fine. What happened?

Your PROMPT_COMMAND includes an OSC 7 / OSC 1337 sequence that reports the current directory to the terminal emulator. Locally, WezTerm (or iTerm2, etc.) intercepts these invisible escape sequences. Over SSH with TERM=linux, no terminal emulator is listening — the raw bytes print as text.

The fix is a guard condition before wiring __report_cwd into PROMPT_COMMAND:

if [[ -n "${TERM_PROGRAM:-}" || "${TERM:-}" =~ xterm|screen|tmux|wezterm|alacritty ]]; then
  [[ ":$PROMPT_COMMAND:" != *__report_cwd* ]] && \
    PROMPT_COMMAND="__report_cwd${PROMPT_COMMAND:+; $PROMPT_COMMAND}"
fi

When TERM=linux (plain SSH) and TERM_PROGRAM is empty, the function is skipped entirely. No escape sequences, no garbage.

After deploying the fix, start a fresh shell — PROMPT_COMMAND is already set in the running session and won't be cleared by re-sourcing.

Run Multiple Claude Code Instances with Separate Configs

Use CLAUDE_CONFIG_DIR to point Claude Code at a different config directory — credentials, settings, sessions all go there instead of ~/.claude.

mkdir -p ~/.claude-work
CLAUDE_CONFIG_DIR=$HOME/.claude-work claude

Set up aliases in your shell config for quick switching:

alias claude-work='CLAUDE_CONFIG_DIR=$HOME/.claude-work claude'

First launch in the new directory triggers the login flow — that's how you get account isolation. Both instances can run simultaneously without interference.

Share session history between instances

CLAUDE_CONFIG_DIR is all-or-nothing — no built-in way to share just sessions. Symlink the projects/ directory back to the original to get shared session history with separate settings:

mkdir -p ~/.claude-work
cp ~/.claude/settings.json ~/.claude-work/settings.json
ln -s ~/.claude/projects ~/.claude-work/projects

This gives you independent config/credentials but a unified session list. New sessions written by either instance appear in both (bidirectional). No read-only option exists — if you need isolation, don't symlink.