Two GitHub accounts on one machine? Let one gh wrapper hand git the right token

If you keep a personal and a work GitHub account on the same machine, you have probably hit this: gh auth login both accounts, run gh auth setup-git, and half your repos start returning Repository not found. That's because gh auth git-credential always answers with the keyring's active account. It never looks at which repo git is asking about.

The fix is to put a small wrapper in front of the real gh. The wrapper picks a token by repo owner, and git uses the same wrapper as its credential helper. Now both gh and plain git go through one place that knows about your accounts.

The heart of the wrapper is a few lines of bash (save as ~/bin/gh-route, keep the real gh on PATH):

#!/usr/bin/env bash
# Pick a token by owner, then run the real gh with it.
owner_from() { sed -E 's#^(https://github.com/|[email protected]:)##; s#/.*##'; }

if [[ "$1 $2" == "auth git-credential" ]]; then
    req="$(cat)"                                   # git's request, on stdin
    owner="$(sed -n 's#^path=\([^/]*\)/.*#\1#p' <<< "$req")"
fi
[[ -n "$owner" ]] || owner="$(git remote get-url origin 2>/dev/null | owner_from)"

case "${owner,,}" in
    my-work-org|my-work-user) export GH_TOKEN="$GH_TOKEN_WORK" ;;
    ?*)                       export GH_TOKEN="$GH_TOKEN_PERSONAL" ;;
esac

if [[ -n "$req" ]]; then gh "$@" <<< "$req"; else gh "$@"; fi

Then wire git to it, and move everything to HTTPS:

# ~/.gitconfig
[credential "https://github.com"]
    helper =
    helper = !bash ~/bin/gh-route auth git-credential
    useHttpPath = true
[url "https://github.com/"]
    insteadOf = [email protected]:
    insteadOf = ssh://[email protected]/

useHttpPath is what makes this reliable. Without it git only tells the helper host=github.com, so the wrapper has to guess from the current directory, and git ls-remote <url> run outside a repo gets the wrong account. With it git sends path=<owner>/<repo>.git, so the owner is right there in the request. (git clone happens to work either way: git writes origin into the new repo and exports GIT_DIR before it asks for credentials.)

The insteadOf lines rewrite existing [email protected]: remotes on the fly, so you don't have to edit a single .git/config. HTTPS also gets through networks that reset SSH, and it's the only transport GitHub App installation tokens work with.

One trap: never run gh auth setup-git again. It appends its own [credential "https://github.com"] block after your include, and its empty helper = line silently wipes out the wrapper.

On Windows (Git Bash), call the wrapper through bash as shown instead of an .exe shim. Git spawning a shim from its own sh can die with a cygheap read copy failed fork error.

Your AI agents page you too often? Give them an oncall agent

When you run a fleet of coding agents, most of their "I'm stuck, human please" pages are not really for you. A runner is wedged, a lock is stale, a relay died, or the question is one you already answered in a runbook or on an old PR. We built an oncall agent that reads every escalation first, acts as you, and only pages you with what truly needs you. The whole thing is small, and it runs on stock my-ai-team (mat) features. It took two days to go from idea to running system.

The flow:

agent ── notify-user --action ──► Telegram (you, unchanged)
   └── ssh oncall-host oncall-inbox        (same message on stdin)
             │
        oncall agent: triage (env / decision / noise), investigate over ssh
             ├── ssh <agent-host> send-relay %<pane> reply.md   → answer the stuck agent
             ├── incident record, committed to a repo
             ├── notify-user                                    → FYI to you
             └── notify-user --action + a GitHub issue          → only when you must decide

1. Forward action messages. On every agent host, point mat at the oncall host (any ssh alias that logs in without a prompt):

git config --global mat.devopsHost oncall-host
…more

Customer subdomains can plant cookies on your login site — the `__Host-` prefix stops it

If you host anything customer-controlled under your own domain (say alice.app.example.com) and run your sign-in or billing site on another subdomain (billing.example.com), those customers can set cookies on your billing site. A cookie with Domain=example.com goes to every host under example.com. Signing your cookies doesn't help, because the attacker doesn't need to forge anything.

The attack is login CSRF by "cookie tossing". The attacker signs in to your site and copies their own valid, signed session cookie. A page on their subdomain sends:

Set-Cookie: session=<attacker's valid value>; Domain=example.com; Path=/

A victim opens that page, then visits your billing site. The browser sends the attacker's cookie, so the victim is signed in as the attacker, and whatever they buy is credited to the attacker's account. Having their own session doesn't reliably protect them either: the browser sends both cookies with the same name, and many parsers keep whichever comes last.

Picking a different hostname doesn't help while both hosts share the same registrable domain. The fix is one prefix on the cookie name:

Set-Cookie: __Host-session=<value>; Path=/; Secure; HttpOnly; SameSite=Lax

Browsers only accept a __Host- cookie that has Secure, has Path=/, and has no Domain attribute. It can only be set by the exact host that receives it, so a sibling subdomain can't create or overwrite it. Read and clear the cookie under the same prefixed name. Since the prefix needs Secure, keep an unprefixed name for plain-http local development.

The stronger, structural fix is to serve customer content from a separate registrable domain, the way GitHub uses githubusercontent.com, or to get that domain listed on the Public Suffix List. Either way, cookies stop crossing the boundary at all. __Host- is the change you can ship today.

ssh host 'cmd' Can't Find Your Tool? Set PATH in ~/.pam_environment

ssh vm 'mytool --version' returns command not found while ssh vm followed by the same command works. The tool is installed, the interactive shell is fine — the difference is which startup files the command actually gets.

ssh vm 'command -v mytool'   # nothing, or only /usr/bin tools
ssh -t vm                     # interactive: mytool is right there

sshd runs a remote command as $SHELL -c 'cmd'. That shell is neither login nor interactive, so on Debian it reads neither ~/.profile (login only) nor ~/.bashrc (which returns at its case $- in *i*) ;; *) return;; esac guard). The session keeps sshd's stock PATH=/usr/local/bin:/usr/bin:/bin:/usr/games. Anything you installed to ~/.local/bin — cargo, pipx, uv tool, most Rust and Python tool installers — is invisible. Chasing it in ~/.bashrc won't work, because ~/.bashrc is not read at all.

The fix that survives every session type is PAM, not shell config. /etc/pam.d/sshd already has a pam_env.so user_readenv=1 line, and pam_env re-reads ~/.pam_environment on every session — no sshd edit, no restart:

cat > ~/.pam_environment <<'EOF'
PATH=/home/you/.local/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
EOF
chmod 600 ~/.pam_environment
ssh vm 'command -v mytool'   # resolves now

Two details bite here. pam_env replaces PATH rather than appending, so the file must spell out the full list, not just the directory you care about. And it must use the bare PATH=… form: PATH DEFAULT=… only applies when the variable is unset, and sshd always exports PATH, so DEFAULT would silently do nothing.

Pair it with a block at the very top of ~/.bashrc, above the non-interactive guard, for nested shells and tmux children that do source .bashrc:

for _pb in "$HOME/.local/bin" "$HOME/bin"; do
    [ -d "$_pb" ] && case ":$PATH:" in *":$_pb:"*) ;; *) PATH="$_pb:$PATH" ;; esac
done
unset _pb
export PATH

The case ":$PATH:" check keeps it idempotent, and it matters more than it looks: once pam_env supplies ~/.local/bin, Debian's own unguarded PATH="$HOME/.local/bin:$PATH" in ~/.profile appends it a second time in every login shell. Harmless, but it misleads the next person reading echo $PATH.

The one case PAM cannot cover is a fully stripped environment — env -i mytool or a systemd unit with a hardcoded PATH= — where even login, interactive and ~/.bashrc are all bypassed. There the only options are to make bash read the file (BASH_ENV=/etc/profile) or put the entry in a system directory, e.g. sudo ln -s ~/.local/bin/mytool /usr/local/bin/mytool, which is already on every default PATH.

Check what a session really sees, rather than guessing:

ssh vm 'echo $PATH; command -v mytool'
ssh vm 'bash -lc "echo $PATH; command -v mytool"'   # login shell

SSH Alias Added, Still Denied? Check the Guest's authorized_keys

A Host block only tells SSH where to connect and which key to offer. A new VM can be reachable and still return Permission denied (publickey) until the guest account trusts that public key.

First confirm the client selected the intended host, user, and identity:

ssh -G vm-alias | grep -E '^(hostname|user|identityfile) '

Then, from the VM console or hypervisor, find the account's real home instead of assuming it is /home/$USER; some Lima guests use a suffixed path:

home=$(getent passwd "$(id -un)" | cut -d: -f6)
install -d -m 700 "$home/.ssh"
cat /path/to/client_key.pub >> "$home/.ssh/authorized_keys"
chmod 600 "$home/.ssh/authorized_keys"

Only the client's .pub key belongs on the guest; keep the private key on the client. Test the complete path with:

ssh -o BatchMode=yes vm-alias 'hostname; id -un'

ssh -G validates client configuration, not server-side authorization.