Two GitLab accounts on one laptop, and git push to the work repo fails with Permission denied (publickey), or authenticates as the wrong account.
The usual fix is swapping keys by hand: rename ~/.ssh/id_rsa, or re-add the other key before every push. It works until you forget.
The rule: the repository's remote URL should select the SSH identity. One host alias per account, one key per alias, nothing to switch by hand.
Scope: gitlab.com, one work and one personal account, Windows and macOS. Not CI keys or key rotation.
Why does Git use the wrong GitLab account?
Both accounts live on the same server, so both remotes look the same to SSH:
git@gitlab.com:company/project.git
git@gitlab.com:username/project.git
Nothing in either URL says which account you mean. SSH connects to gitlab.com as user git both times. If your agent holds both keys, SSH can offer more than one, and GitLab authenticates whichever it accepts first.
The model behind key swapping is "SSH has one identity, and I change it before I push." That turns the active account into hidden global state. Nothing in the repository records which account it belongs to, so every push depends on what you did last.
What should choose the key instead?
Treat it as routing, not switching. The repository names an alias, the alias names a key, and the key belongs to one account.
git pull
↓
origin = git@gitlab-work:company/project.git
↓
SSH config: Host gitlab-work
↓
IdentityFile ~/.ssh/id_gitlab_work
↓
local proof made with the private key
↓
GitLab verifies it with the public key on the work account
gitlab-work is not a DNS hostname. It is a name in your SSH config that maps to gitlab.com plus one specific key.
Two facts keep this model honest:
- The private key never leaves your machine. SSH uses it locally to produce a proof (a signature). GitLab checks that proof with the public key you registered.
-
ssh-agentis optional. It keeps an unlocked key available locally so you don't retype the passphrase. It does not send the key anywhere.
How do you set up one key and one alias per account?
1. Generate two keys and load them. Give each a passphrase. The -C comment is only a label; it does not decide which account gets the key.
Windows (PowerShell; the two service lines need an elevated session):
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.ssh"
ssh-keygen -t ed25519 -C "work@example.com" -f "$env:USERPROFILE\.ssh\id_gitlab_work"
ssh-keygen -t ed25519 -C "personal@example.com" -f "$env:USERPROFILE\.ssh\id_gitlab_personal"
Get-Service ssh-agent | Set-Service -StartupType Automatic
Start-Service ssh-agent
ssh-add "$env:USERPROFILE\.ssh\id_gitlab_work"
ssh-add "$env:USERPROFILE\.ssh\id_gitlab_personal"
ssh-add -l
macOS:
ssh-keygen -t ed25519 -C "work@example.com" -f ~/.ssh/id_gitlab_work
ssh-keygen -t ed25519 -C "personal@example.com" -f ~/.ssh/id_gitlab_personal
ssh-add --apple-use-keychain ~/.ssh/id_gitlab_work
ssh-add --apple-use-keychain ~/.ssh/id_gitlab_personal
ssh-add -l
A running agent and a loaded key are different states. Trust ssh-add -l, not the service status.
2. Register each public key with the matching account. id_gitlab_work.pub goes into the work account's SSH key settings, id_gitlab_personal.pub into the personal one. Never the file without .pub. (GitLab SSH docs)
3. Write ~/.ssh/config.
Host gitlab-work
HostName gitlab.com
User git
IdentityFile ~/.ssh/id_gitlab_work
IdentitiesOnly yes
Host gitlab-personal
HostName gitlab.com
User git
IdentityFile ~/.ssh/id_gitlab_personal
IdentitiesOnly yes
-
Hostis the alias you will put in the remote URL. -
HostNameis the real server. -
User gitis the SSH user GitLab expects. -
IdentityFileis the private key for this alias. -
IdentitiesOnly yestells SSH to use the identity configured here instead of offering every key the agent holds. It does not turn the agent off.
On macOS, add AddKeysToAgent yes and UseKeychain yes under each Host for Keychain integration. Those two lines are macOS-only.
4. Point each repository at its alias.
git remote set-url origin git@gitlab-work:company/project.git
Personal repositories use git@gitlab-personal:username/project.git. From here on, the remote is part of the authentication design, not just metadata.
How do you prove each layer before the first push?
Test SSH directly before you debug Git. If ssh -T fails, Git is not the problem yet.
| Command | Expect | What it proves |
|---|---|---|
ssh -G gitlab-work |
hostname gitlab.com, user git, your work identityfile, identitiesonly yes
|
SSH reads the alias the way you meant |
ssh -T git@gitlab-work |
GitLab's welcome message for your work username | The work key authenticates as the work account |
ssh -T git@gitlab-personal |
The welcome message for your personal username | The personal key authenticates as the personal account |
git remote -v |
git@gitlab-work:... for fetch and push |
This repository routes through the right alias |
git fetch |
No error, no prompt for the other key | The whole chain works through Git |
On the first connection, check the host key fingerprint against GitLab's published fingerprints before you type yes.
Why can ssh -T pass while Git still prompts or fails?
This one cost me the most time, on Windows.
My machine had MSYS2's OpenSSH installed next to Windows OpenSSH. Both keys were loaded in the Windows agent, and I had already cleaned up PATH. Git still kept asking for the key passphrase.
The lesson: the ssh your terminal resolves is not proof of which SSH command Git runs. Check both:
Get-Command ssh
where.exe ssh
git config --show-origin --get core.sshCommand
$env:GIT_SSH_COMMAND
On macOS the first two are command -v ssh and which -a ssh. If GIT_SSH_COMMAND is set, it overrides core.sshCommand (git-config).
What fixed it on that machine was telling Git exactly which client to launch:
git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"
After that, Git used the same OpenSSH as the Windows agent, and the prompts stopped. This setting does not change the protocol, move keys, or replace your SSH config. It only makes Git's choice of SSH executable explicit.
That is one machine, not a rule. If your default setup already works, leave it alone.
Where else does this break?
-
An old remote. A repository still on
git@gitlab.com:...bypasses both aliases. Rungit remote -vfirst. - The IDE terminal. An IDE can inherit a different PATH and different agent access than your system terminal. Run the same checks inside it before blaming the config.
-
Commit author. The SSH identity decides which account authenticates.
user.nameanduser.emaildecide what gets written into commits. Fixing one does nothing for the other. -
macOS. I ran the Windows path on my own machine. The macOS steps are checked against current documentation, not run by me on a Mac. If
--apple-use-keychainbehaves differently on your version, readssh-add's own help. -
Shortcuts that make it worse.
StrictHostKeyChecking=noand a passphrase stored in.envboth make the prompt go away by removing the protection.
What should you check in the next 20 minutes?
- Run
git remote -vin one work repo and one personal repo. Is anything still ongit@gitlab.com:? - Run
ssh -G gitlab-work. Doidentityfileandidentitiesonlysay what you intended? - Run
ssh -Tagainst each alias. Does each one greet the right username? - Run
git config --show-origin --get core.sshCommandand checkGIT_SSH_COMMAND. Do you know whichsshGit launches? - Run
git config user.emailin each repo. Does the commit identity match the account?
Next: debugging Permission denied (publickey) one layer at a time.
Has Git ever run a different ssh than your terminal did? What was installed, and how did you catch it?




Top comments (0)