I have a personal GitHub account, an account for my own company, and one for each client I work with.
They all live on the same laptop, and they all talk to github.com.
When having multiple accounts, you need to be careful about using the right SSH key and the right commit email address for each repository. But if you plan it right, you can make it so that Git does the right thing automatically, without you having to remember which account a repository belongs to.
In this blog post, I show the setup: one base directory per account, and no special URLs. I clone with the exact URL that GitHub puts on my clipboard.
One base directory per account
Everything is based on one rule: the directory a repository lives in determines which account it belongs to.
~/Projects
├── wimdeblauwe/ (1)
├── forks/ (2)
├── widit/ (3)
└── bigcorp/ (4)
| 1 | My personal repositories. |
| 2 | Forks of other people’s projects, also under my personal account. |
| 3 | My own company. |
| 4 | A client. |
|
Two directories can map to the same account, as |
To make it work, we need to configure both SSH and Git to use the right identity for each directory.
Step 1: one SSH key per account
You cannot reuse a single SSH key for multiple GitHub accounts. GitHub rejects a public key that is already registered on another account. You can’t work around it: the key is the identity that GitHub authenticates you with.
So generate one public key per account:
ssh-keygen -t ed25519 -f ~/.ssh/wimdeblauwe-git -C "wim.deblauwe@gmail.com"
ssh-keygen -t ed25519 -f ~/.ssh/widit-git -C "wim@widit.be"
Then add each public key to the matching account under Settings → SSH and GPG keys → New SSH key.
I keep my keys in 1Password and use its SSH agent, so the private keys are not on disk at all.
Only the .pub files are, and that is all the configuration below needs.
If you use 1Password with the browser extension, you can ask 1Password to create the keys for you and add them to GitHub automatically at that page on GitHub.
|
Step 2: give every account its own SSH host
SSH cannot guess which key belongs to which account, because as far as SSH is concerned there is only one server: github.com.
The fix is to invent a hostname per account in ~/.ssh/config:
Host gh-wimdeblauwe
HostName github.com (1)
User git
IdentityFile ~/.ssh/wimdeblauwe-git.pub
IdentitiesOnly yes (2)
Host gh-widit
HostName github.com
User git
IdentityFile ~/.ssh/widit-git.pub
IdentitiesOnly yes
| 1 | gh-wimdeblauwe and gh-widit are not real hostnames.
They both resolve to github.com, they just carry a different IdentityFile. |
| 2 | Without this, your agent offers every key it has and GitHub authenticates you as the owner of the first one that matches. That is exactly the bug we are trying to avoid, so do not leave it out. |
Pointing IdentityFile at the .pub file looks odd, but it is the correct thing to do when the private key lives in an agent like the 1Password one.
SSH uses the public key to ask the agent for a signature.
If your private keys are on disk, point at the private key instead.
|
We can now check if the SSH configuration works by connecting to each host:
$ ssh -T git@gh-wimdeblauwe
Hi wimdeblauwe! You've successfully authenticated, but GitHub does not provide shell access.
$ ssh -T git@gh-widit
Hi wimdeblauwe-widit! You've successfully authenticated, but GitHub does not provide shell access.
If both lines greet you with the same username, IdentitiesOnly yes is missing somewhere.
At this point you have a working, but annoying, setup.
If you want to clone a repository from the widit account, you cannot just copy-paste the URL from GitHub.
GitHub will give you git@github.com:widit/some-service.git, but you need to manually change it to git@gh-widit:widit/some-service.git to make it work.
The next step fixes that.
Step 3: select a config file per base directory
Create (or edit) ~/.gitconfig to link to the per-account configuration files:
[init]
defaultBranch = main
[includeIf "gitdir:~/Projects/wimdeblauwe/"] (1)
path = ~/Projects/wimdeblauwe/.gitconfig (2)
[includeIf "gitdir:~/Projects/forks/"]
path = ~/Projects/forks/.gitconfig
[includeIf "gitdir:~/Projects/widit/"]
path = ~/Projects/widit/.gitconfig
[includeIf "gitdir:~/Projects/bigcorp/"]
path = ~/Projects/bigcorp/.gitconfig
| 1 | The trailing slash matters. gitdir:~/Projects/wimdeblauwe/ means "this directory and everything below it".
Without the slash it only matches a repository called wimdeblauwe itself. |
| 2 | I keep the included file inside the directory it describes, but that is only a convention. Any path works. |
Step 4: the per-account configuration
Each .gitconfig file contains the account-specific settings:
[user]
email = wim@widit.be
name = Wim Deblauwe
[url "git@gh-widit:"] (1)
insteadOf = git@github.com:
[github]
user = "wimdeblauwe-widit" (2)
| 1 | This avoids the need to manually change the clone url, see below. |
| 2 | Not used by Git itself, but several tools (GitHub CLI wrappers, IDE plugins) pick it up. |
And the personal one, which is identical apart from the values:
[user]
email = wim.deblauwe@gmail.com
name = Wim Deblauwe
[url "git@gh-wimdeblauwe:"]
insteadOf = git@github.com:
[github]
user = "wimdeblauwe"
~/Projects/forks/.gitconfig contains the exact same content as the personal one, because both directories belong to the same account.
url.<base>.insteadOf tells Git: whenever you are about to use a URL that starts with git@github.com:, replace that prefix with git@gh-widit: first.
The important word is use.
The rewrite happens at the moment Git contacts the remote, not at the moment the URL is stored.
So the URL in .git/config stays exactly what you typed:
$ cd ~/Projects/wimdeblauwe/wimdeblauwe.com
$ git config remote.origin.url
git@github.com:wimdeblauwe/wimdeblauwe.com.git
…while the connection actually goes through the alias:
$ GIT_TRACE=1 git fetch 2>&1 | grep start_command
trace: start_command: /usr/bin/ssh -o SendEnv=GIT_PROTOCOL git@gh-wimdeblauwe 'git-upload-pack ...'
Bonus: signing keys per account
Anything else that is account-specific belongs in the same file. For the accounts where commit signing is required, my per-account config also contains:
[user]
signingkey = ~/.ssh/bigcorp-git.pub (1)
[gpg]
format = ssh
[gpg "ssh"]
program = "/Applications/1Password.app/Contents/MacOS/op-ssh-sign"
[commit]
gpgsign = true
| 1 | With gpg.format = ssh, signingkey accepts the path to the public key when the private key lives in an agent, so there is no key material in the config file. |
Signing is on for that client and off everywhere else, without me having to think about it.
Conclusion
The setup is four small pieces: a key per account, an SSH host alias per key, an includeIf per base directory, and an insteadOf per account.
I can now clone anything by pasting the URL GitHub gives me into the right directory.
If you have any questions or remarks, feel free to post a comment at GitHub discussions.