Posts tagged with “tools”

Got personal files to ignore? Use .git/info/exclude, not the shared .gitignore

When you want Git to ignore something that's yours — a local scratch dir, editor cruft, a personal plans/ folder, machine-local config — don't add it to the tracked .gitignore. That file is shared: a rule like plans/ or wiki-drafts/ forces your personal directory conventions onto everyone, and they don't have those dirs.

Put it in .git/info/exclude instead. Same syntax as .gitignore, but it's per-clone and never committed — so it's invisible to teammates and travels with nothing:

# .git/info/exclude
.claude/settings.local.json
.claude/plans/
.claude/wiki-drafts/

The rule of thumb: **.gitignore is for what the project should ignore (build output, node_modules/, *.user); .git/info/exclude is for what you happen to ignore on this machine.** If a teammate cloning fresh wouldn't also produce the file, it doesn't belong in the shared list.

One gotcha with worktrees: info/exclude lives in the common git dir, so it's shared across all linked worktrees of the same clone — but still not across separate clones. Find it with git rev-parse --git-common-dir if you're not sure where it is.

And if you ever need to start tracking something a blanket exclude was hiding (e.g. you had .claude excluded but now want .claude/skills/ committed), narrow the exclude line to the specific paths you still want ignored — don't delete it and shove replacement rules into the shared .gitignore.

Shell scripts can't `cd` for you — source them instead

A shell script always runs in a subshell. Any cd it executes vanishes when the script exits — the calling shell's working directory is unchanged.

The fix: source the script instead of executing it. Sourcing runs the script in the current shell, so cd sticks.

alias sw='. ~/bin/sw'

Two things to update in the script itself.

First, replace every exit with return. exit in a sourced script exits the entire shell, not just the script:

exit 1  →  return 1

Second, $0 in a sourced script is the shell binary (bash), not the script path. Use ${BASH_SOURCE[0]} wherever you need the script's own location:

source "$(dirname "${BASH_SOURCE[0]}")/git-common.sh"
script_name=$(basename "${BASH_SOURCE[0]}")

The one tradeoff: functions defined inside the script leak into the shell's namespace after it returns. For a personal utility this is usually fine.

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.