writing / / 4 min / git · rust
Commits keep landing under the wrong name
Work account, personal account, a client's GitLab — and one global user.email quietly signing all of it. Why I built GUM, a Rust CLI that picks your git identity from the repo's remote.
Here's a failure mode most engineers with more than one git account know intimately. You clone a work repo, fix something, commit, push. Weeks later someone points out that the commit — sitting in your employer's history forever — is signed you@personal.dev. Or the inverse: your open-source contributions are credited to a work address that will stop existing the day you change jobs. If commit signing is involved, it's worse — now the key doesn't match the email either.
None of this breaks the build. That's exactly why it keeps happening: nothing fails at the moment of the mistake. Git happily commits as whoever user.email says you are, and user.email is usually whatever your global config happened to say when you set the laptop up.
What git gives you out of the box #
Git's built-in answer is the conditional include — point a directory at a config file:
[includeIf "gitdir:~/work/"]
path = ~/.config/git/work.gitconfig
This works, and if it works for you, keep it. But it encodes an assumption I kept tripping over: that identity follows the folder. It only holds as long as you file every repo in the right place. Clone a work repo into ~/tmp to test something quickly and you're back to the global default — silently. The filing system is the mechanism, and I am not a reliable filing system.
Newer gits can do better: since 2.36 there's includeIf "hasconfig:remote.*.url:...", which keys the include on the remote URL instead of the directory. That's the right axis. But it still only swaps config files you write and maintain by hand — and it says nothing about the other half of the problem, which is SSH.
Identity belongs to the remote #
That axis — the remote, not the folder — is the idea GUM is built around. A repo's identity was never really about where it lives on disk; it's about where it pushes. git@github.com:work-org/payments.git is a work repo whether it's checked out in ~/work, ~/tmp, or on a USB stick.
GUM keeps named profiles — name, email, SSH key, signing config — gives each one a glob pattern over remote URLs, and writes all the plumbing for you instead of leaving you to hand-maintain config files:
$ gum add work --user-name "Ada" --email ada@work.dev \
--remote 'git@github.com:work-org/**' \
--ssh-key ~/.ssh/id_work --auto-sign
$ gum auto enable work
✓ Auto-switching enabled for work
From then on, any repo whose origin matches the pattern gets the right local config — clone it anywhere:
$ cd ~/anywhere/payments
$ git config user.email
ada@work.dev
The boring parts are the point #
Name and email are only half the identity problem. The other half is SSH: two GitHub accounts can't share a key, so multi-account setups end up with host aliases in ~/.ssh/config and remotes that have to remember to use them. GUM manages those host-alias blocks and sets per-repo core.sshCommand itself, so the remote URL you clone is the remote URL you use. And because config that mutates itself needs a way to be inspected, there's a gum doctor that audits the whole setup — flagging plaintext tokens and half-finished configs before they bite.
It's written in Rust — partly because that's what I write all day, mostly because a tool like this should be a single static binary that starts instantly and has no runtime to drag along. It's open source, installable with brew install ioanSL/tap/git-user-manager, and has a proper landing page that is considerably more bubblegum than this site.
If you've ever apologised in a pull request for the wrong name on a commit: it's not a discipline problem, it's a tooling problem. Make the machine remember.