Permission denied (publickey): 6 Reasons Git Push Fails Over SSH (and the Fix for Each)
Symptom: git refuses to talk to the remote
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.
One-line diagnosis: SSH authentication failed — the server doesn't recognize the public key you presented. This has nothing to do with passwords (publickey auth doesn't use one). Either the key in your hand doesn't match any lock registered on the server, or you never handed over a key at all.
First step: test auth directly with ssh -T
Skip git and test the SSH channel itself — you'll get the cleanest error:
ssh -T git@github.com
# Success: Hi yourname! You've successfully authenticated...
# Failure: git@github.com: Permission denied (publickey).
# Add -v for the full story: which keys were offered, which auth the server accepts
ssh -vT git@github.com 2>&1 | grep -E 'Offering|identity file|denied'
The Offering public key: /Users/you/.ssh/id_xxx lines in the -v output are the key evidence — they list the keys actually presented. If the one you thought you were using isn't there, jump straight to cause 2.
Six causes, ranked by frequency
1. No key on this machine (most common on a new laptop)
ls ~/.ssh/
# id_ed25519 / id_ed25519.pub or id_rsa / id_rsa.pub → you have one
# nothing → generate one
ssh-keygen -t ed25519 -C "your_email@example.com"
# accept the defaults (or set a passphrase), then start the agent and load it
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
Then add the contents of the .pub file (public keys only — the private key never leaves your machine) to GitHub: Settings → SSH and GPG keys → New SSH key.
2. Key exists, but the agent isn't offering it
ssh-add -l
# "The agent has no identities." → not loaded
ssh-add ~/.ssh/id_ed25519
On macOS, put the key metadata in ~/.ssh/config and skip the manual add forever:
Host github.com
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519
3. Public key never added to the platform (or added to the wrong account)
GitHub keys are account-scoped: a key registered under account A gets rejected for account B's repos. Another trap: the same public key is already bound to a different GitHub account — GitHub doesn't allow one key on multiple accounts, and the same applies to deploy keys. For multi-account setups, see cause 4.
Company GitLab, Gitee, and self-hosted servers each have their own key-upload page — make sure you added it to the platform you're actually pushing to.
4. Multiple accounts/keys: the wrong key gets offered
With a work key and a personal key on the same machine, SSH tries them in order and often presents the wrong one. Route by host alias in ~/.ssh/config:
# Personal GitHub
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_personal
IdentitiesOnly yes
# Work GitHub account via an alias
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
IdentitiesOnly yes is the critical line — it forces SSH to offer only the specified key instead of trying every key it can find. Then use the alias in your remote URL:
git remote set-url origin git@github-work:company/repo.git
5. Deploy keys have a permission boundary
A public key added under a repo's Settings → Deploy keys is valid for that repo only: cloning a different repo with it, or pushing when the key was added without write access, both end in publickey rejection. The classic CI trap: one runner using one deploy key to pull many repos — use organization-level deploy keys or a machine-user account instead.
6. Wrong remote URL (https and ssh mixed up)
git remote -v
# origin git@github.com:user/repo.git → SSH; debug with the methods above
# origin https://github.com/user/repo.git → HTTPS; errors ask for a password/token, not publickey
Case mismatches, misspelled org names, and a missing .git suffix all blur the line between "repo not found" and "auth failed". Also note: if a GitHub organization enforces SSO, a personal key still needs a one-time authorization for that org (SSO → Authorize).
Why does it say publickey when I never configured keys?
The word in parentheses is the list of auth methods the server permits. If you see Permission denied (publickey,password), both methods were tried and both failed — SSH attempts each method the server allows before giving up. Read the parentheses to see which doors exist.
Checklist
ssh -T git@github.com— direct test, cleanest error messagessh -vT ... | grep Offering— which keys were actually presented?ls ~/.ssh/+ssh-add -l— does the key exist? is it loaded?- Is the public key registered on the right platform, account (or repo deploy key)?
- Multiple keys:
~/.ssh/configwith per-host aliases +IdentitiesOnly yes git remote -v— check the URL spelling and protocol (ssh vs https)
Provided by ToolVault. Related tools: Linux Cheatsheet, Password Generator, JWT Decoder. Related reading: npm registry mirror guide, Port already in use. See the homepage for more developer tools.
Related Tools
Related Articles
error:0308010C digital envelope routines::unsupported — Fixing Node 17+ Breakage in Older Projects
Node 17+ crashes webpack 4 builds with error:0308010C:digital envelope routines::unsupported because OpenSSL 3.0 removed MD4. Three fixes compared: --openssl-legacy-provider quick unblock, upgrading to webpack 5 as the real fix, and pinning Node 16 as a stopgap.
ECONNREFUSED: Connection Refused — 5 Causes Explained (Including Docker)
Node, Java, or curl reporting connect ECONNREFUSED 127.0.0.1:3306? It means nothing is listening on that port. Covers service not running, wrong port, 127.0.0.1-only binding, Docker container networking, and firewall REJECT rules — with ss/lsof diagnostic commands.
Docker permission denied on docker.sock: the Right Fix (and Its Security Cost)
docker commands fail with permission denied while trying to connect to the Docker daemon socket? The cause: your user isn't in the docker group. One-step fix with usermod -aG, the newgrp trick to avoid re-login, why chmod 777 is a trap, and why docker group membership equals passwordless root — plus the rootless alternative.