GithubTechnology

How to Painlessly Run Multiple GitHub Accounts on One Machine

To start, I run multiple GitHub accounts on one machine. Specifically, there is a personal account, two accounts tied to my own companies, and a work account. Naturally, each one wants its own SSH key. In addition, each one wants its own commit email, and sometimes its own signing key. For years, as a result, my git commands grabbed the wrong key or stamped commits with the wrong identity. So I never had a setup I trusted to just work on clone.

Eventually, I landed on a layered setup. Basically, it routes everything automatically from the GitHub org or user in the repo URL. As a result, multiple GitHub accounts stop fighting over keys, and there is no manual step after cloning. In this post, therefore, I walk through each piece: the git URL rewrites, the SSH host aliases they point at, the per-identity config, and the GitHub CLI. The CLI matters too, because it picks a user a completely different way. Also, note that all org, user, and key names below are placeholders. Finally, if you also juggle machines, my notes on moving to a new Mac cover some related setup.

for multiple Github accounts

The old hack for multiple GitHub accounts, and why it annoyed me

At first, my workaround was simple but clumsy. Specifically, I defined a few aliased hosts in ~/.ssh/config. They all pointed at github.com, but each used a different key. Then, after cloning a repo, I manually rewrote its remote to use the alias. For example, I changed git@github.com:myuser/repo.git to git@myuser-github:myuser/repo.git.

However, the trouble was that one word: manually. In fact, every fresh clone needed that edit first. Otherwise, I could not push or pull. Worse still, some private repos were not even reachable until I switched the remote to the right key. So it worked, but it leaked a manual step into everything.

The insight: route by the org or user in the URL

For a long time, I ignored the obvious. Yet the org or user is always right there in the URL. For instance, a MyCompany repo is github.com/MyCompany/…, and my personal repos are github.com/myuser/…. Therefore, that prefix is enough to decide which key and identity to use. Better still, git can make that decision for me with insteadOf.

Git user woes

Layer 1: URL rewrites for multiple GitHub accounts

In ~/.gitconfig, I start with one global rule. Basically, it forces every GitHub HTTPS URL to SSH. After that, I add one block per org or user. In turn, each block rewrites that org’s URLs to a dedicated SSH host alias.

# Force all github.com HTTPS URLs to SSH
[url "git@github.com:"]
    insteadOf = https://github.com/

# Personal account
[url "git@myuser-github:myuser/"]
    insteadOf = git@github.com:myuser/
    insteadOf = https://github.com/myuser/

# MyCompany org
[url "git@mycompany-github:MyCompany/"]
    insteadOf = git@github.com:MyCompany/
    insteadOf = https://github.com/MyCompany/

# ClientCompany org (clientuser account)
[url "git@clientcompany-github:ClientCompany/"]
    insteadOf = git@github.com:ClientCompany/
    insteadOf = https://github.com/ClientCompany/

# AnotherCompany / GitHub Enterprise Cloud (work account)
[url "git@anothercompany-github:AnotherCompany/"]
    insteadOf = git@github.com:AnotherCompany/
    insteadOf = https://github.com/AnotherCompany/

Two details make this work. First, each block lists insteadOf twice. Specifically, one entry covers the SSH form, and the other covers the HTTPS form. As a result, it does not matter which URL style I start from. Also, insteadOf is a multi-valued key, so repeating it is expected rather than a mistake.

Second, and this part confused me for a while, git applies the longest matching prefix. So for a personal URL, two rules match: the global git@github.com: rule and the more specific git@github.com:myuser/ rule. In the end, the specific one wins, simply because it is longer. In other words, the global rule is just a fallback that flips anonymous HTTPS to SSH. Meanwhile, the per-org rules override it to select an identity.

One caveat is worth knowing. Notably, that global rewrite applies to every GitHub URL. Therefore, it also catches third-party and public repos that you might want to clone anonymously. In those cases, git quietly pushes them through SSH and your default key. So if a clone of someone else’s public repo suddenly asks about SSH keys, this is why.

Layer 2: SSH host aliases for multiple GitHub accounts

The rewrites above point at host aliases such as myuser-github. However, those are not real hostnames. Instead, they are entries in ~/.ssh/config. Notably, each one sets HostName github.com, yet hands SSH a different key.

# Personal account
Host myuser-github
  HostName github.com
  User git
  IdentityFile ~/.ssh/myuser-github

# MyCompany org (reuses the personal key, since it is my account)
Host mycompany-github
  HostName github.com
  User git
  IdentityFile ~/.ssh/myuser-github

# ClientCompany org (clientuser account)
Host clientcompany-github
  HostName github.com
  User git
  IdentityFile ~/.ssh/clientuser-github

# Optional fallback when someone uses a bare github.com host
Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/default-github

On the server side, GitHub figures out which account you are from the key you authenticate with. For that reason, pointing each alias at a different IdentityFile is what actually selects the user. For more on key setup, see GitHub’s SSH documentation. Meanwhile, on my machine, an SSH agent serves the keys. So the global section of my config simply points every host at the agent socket.

Host *
  IdentityAgent ~/.ssh/agent.sock
  IdentitiesOnly yes
  PreferredAuthentications publickey

Here is the chain so far. First, the org in the URL selects an insteadOf rule. Next, that rule rewrites to a host alias. Then the alias selects an IdentityFile. Finally, that key identifies the account to GitHub.

Layer 3: the right name, email, and signing key per repo

Keys handle transport and authentication. However, identity also means the name and email on your commits, plus the key that signs them. So routing the network layer solves only half of the problem for multiple GitHub accounts. For the rest, therefore, I use per-account git config through conditional includes in ~/.gitconfig. Conveniently, modern git can match on a repo’s remote URL with hasconfig:remote.*.url.

[includeIf "hasconfig:remote.*.url:git@github.com:MyCompany/**"]
    path = ~/.gitconfig-mycompany
[includeIf "hasconfig:remote.*.url:https://github.com/MyCompany/**"]
    path = ~/.gitconfig-mycompany
[includeIf "hasconfig:remote.*.url:git@github.com:myuser/**"]
    path = ~/.gitconfig-mycompany
[includeIf "hasconfig:remote.*.url:https://github.com/myuser/**"]
    path = ~/.gitconfig-mycompany

Then each included file sets the identity for that persona.

# ~/.gitconfig-mycompany
[user]
    email = me@mycompany.com
    signingkey = <your ssh signing key>

Notice that I match on the remote URL rather than the directory path. As a result, it does not matter where on disk I cloned the repo, because the identity follows the org. Still, one gotcha caught me here. Specifically, an SSH key used for authentication is not automatically usable for signing. So if you sign with SSH (gpg.format = ssh), you must also register the key as a signing key on the account. Otherwise, commits will not show as verified.

Putting it together: clone and go

With all three layers in place, a normal clone just works. In fact, there is no remote surgery afterward. Better yet, it works whether I start from the SSH or the HTTPS form.

$ git clone git@github.com:myuser/example-repo.git
Cloning into 'example-repo'...
remote: Enumerating objects: 1971, done.
Receiving objects: 100% (1971/1971), 2.69 MiB | 1.55 MiB/s, done.
Resolving deltas: 100% (393/393), done.

$ cd example-repo
$ git remote -v
origin  git@myuser-github:myuser/example-repo.git (fetch)
origin  git@myuser-github:myuser/example-repo.git (push)

$ git config user.email
me@mycompany.com

As you can see, the remote already shows the rewritten alias. Likewise, the commit email is already the right identity. Indeed, git chose both automatically, just from the myuser/ prefix. In other words, multiple GitHub accounts now feel like one. And if you ever tangle up a checkout, my older note on resetting a Git branch still comes in handy.

Where the GitHub CLI fits, and how it picks a user

This part tripped me up the most. The reason is simple: gh decides which user on a completely different axis. Specifically, the key routing above keys off the org prefix in each URL. However, gh ignores the org and my SSH aliases entirely. Instead, it keys off the real host, github.com, plus an active account. Moreover, it stores that account per host in ~/.config/gh/hosts.yml.

github.com:
    git_protocol: ssh
    users:
        myuser:
        clientuser:
            git_protocol: https
        workuser:
    user: myuser

There are a few things to read out of that file. First, gh can log into several accounts for one host at once. Then the users: list shows them all. Meanwhile, the top-level user: is the active one. So commands like gh pr create use the active account’s token, no matter which org owns the repo. Therefore, to switch the active account, you run gh auth switch.

$ gh auth status            # list accounts, show which is active
$ gh auth switch --user myuser

You can also set git_protocol per user. For example, clientuser uses https while the host default is ssh. In addition, my ~/.gitconfig wires gh in as a credential helper for HTTPS.

[credential "https://github.com"]
    helper =
    helper = !gh auth git-credential

Because of that, any HTTPS git operation that is not rewritten to SSH borrows the active account’s token from gh. So in practice, two independent identity systems run side by side. First, one picks SSH keys by org prefix. Meanwhile, the other picks the gh active account by host.

The trap: two systems that can disagree

Naturally, those two systems can fall out of sync. Recently, for example, my push and key routing worked perfectly for my personal account. However, gh pr create still failed with an authorization error. In fact, the cause was simple: gh‘s active account was a work account, workuser, which could not see my personal repo. As a result, a quick gh auth switch --user myuser fixed it. So if git works but gh fails, or the reverse, first check that the active gh account matches your SSH routing.

Quick verification checklist for multiple GitHub accounts

Whenever I add a new identity, I sanity-check the whole chain. In short, these four commands cover it.

# 1. The remote got rewritten to the right alias
git remote -v

# 2. The right key authenticates (prints the GitHub username)
ssh -T git@myuser-github

# 3. The right identity is active in the repo
git config user.email

# 4. gh is pointed at the matching account
gh auth status

Why this beats juggling multiple GitHub accounts

In the end, running multiple GitHub accounts on one machine comes down to a declarative, prefix-driven setup. As a result, cloning any repo, from any account, is a plain git clone with no follow-up. Moreover, the key, the commit identity, and the signing key all follow the org automatically. Still, the one manual lever left is the gh active account, which I keep in sync when I drive things from the CLI. Ultimately, after years of fighting it, my git split personalities finally behave.

Hi, I’m Rob Weaver