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
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.
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.
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.
ss -tlnp and lsof -i :PORT tell you the PID and command name, but for Node.js or Python processes, "node" or "python" alone doesn't tell you which project is running. The working directory is what you actually need — and it's sitting right there in /proc/$pid/cwd.
Put this in ~/.bashrc:
whoseport() {
if [ -z "$1" ]; then
sudo ss -tlnp | tail -n +2 | while read -r line; do
port=$(echo "$line" | grep -oP ':\K[0-9]+(?=\s)')
pid=$(echo "$line" | grep -oP 'pid=\K[0-9]+' | head -1 | tr -dc '0-9')
[ -n "$pid" ] && echo "PORT: $port | PID: $pid | CMD: $(ps -p $pid -o comm=) | CWD: $(readlink /proc/$pid/cwd)"
done
else
whoseport | grep 'PORT: '$1
fi
}
Usage:
$ whoseport 6173
PORT: 6173 | PID: 1326886 | CMD: node | CWD: /home/davidw/Projects/ccode_viewer/server
$ whoseport # list all listening ports
PORT: 61217 | PID: 1356 | CMD: tailscaled | CWD: /
PORT: 22 | PID: 1385 | CMD: sshd | CWD: /
A few things that went wrong before arriving at this version:
Don't use an alias for this — $1 in a single-quoted alias gets swallowed by inner sh -c calls, and local variables don't survive xargs boundaries. A function avoids both problems. The tr -dc '0-9' on the PID is not cosmetic — ss output can carry trailing whitespace or newlines that break ps -p with a "process ID list syntax error".