If Git or SSH stops with Permission denied (publickey)., the full output usually looks like this:
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.
The second half of that message is misleading. The repository almost certainly exists and your access rights are probably fine. Permission denied (publickey) means the server accepted your TCP connection, asked for a public key, and rejected every key your client offered — so it hung up before authentication finished. The fatal: line is just Git reporting that the transport died.
Test authentication on its own, without Git in the way:
ssh -T git@github.com
A working setup prints your username:
Hi yourname! You've successfully authenticated, but GitHub does not provide shell access.
That "does not provide shell access" wording is the success case, not a failure. If you get Permission denied (publickey). instead, work down the list below in order.
Why This Happens
There are only four real causes, in rough order of frequency:
- No key exists — a fresh machine or a fresh user account has an empty
~/.ssh. - The key is not being offered — it exists, but it is not loaded in the agent and has a non-default filename, so SSH never tries it.
- The public key is not registered on the server — the key is offered, and the server does not recognise it.
- File permissions are too open — OpenSSH refuses to use a private key other users can read, and falls back to offering nothing.
Fix 1: Check Whether You Have a Key At All
ls -la ~/.ssh
You are looking for a matching pair, such as id_ed25519 (private) and id_ed25519.pub (public). Older setups use id_rsa / id_rsa.pub.
If the directory is empty or does not exist, generate a key:
ssh-keygen -t ed25519 -C "your@email.com"
Press Enter to accept the default path. Set a passphrase if you want one — the agent will remember it. On a legacy system that rejects Ed25519, use ssh-keygen -t rsa -b 4096 -C "your@email.com" instead.
Fix 2: Register the Public Key on the Server
Copy the public key — the file ending in .pub:
cat ~/.ssh/id_ed25519.pub
That prints one line beginning ssh-ed25519 AAAA.... Paste it into your host's key settings:
- GitHub — Settings → SSH and GPG keys → New SSH key
- GitLab — Preferences → SSH Keys
- Bitbucket — Personal settings → SSH keys
- Your own server — append it to
~/.ssh/authorized_keyson that machine
Never paste the file without the
.pubextension. That is your private key. If you have already uploaded one, delete it from the service and generate a fresh pair — a private key that has left your machine must be treated as compromised.
Fix 3: Load the Key Into the SSH Agent
A key that exists but is not loaded will not be offered. Check the agent:
ssh-add -l
If it prints The agent has no identities., add your key:
ssh-add ~/.ssh/id_ed25519
On macOS, store the passphrase in the keychain so it survives reboots:
ssh-add --apple-use-keychain ~/.ssh/id_ed25519
If ssh-add itself fails with Could not open a connection to your authentication agent, the agent is not running:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
This is the cause behind the classic "it worked yesterday, now it doesn't" report — the agent is cleared on reboot.
If you use a password manager as your agent rather than OpenSSH's own, the diagnosis changes: an empty ssh-add -l or an agent refused operation error usually means the integration itself has stopped serving keys, not that your key is missing. See 1Password SSH agent not working for that path.
Fix 4: Repair File Permissions
OpenSSH refuses to use an over-permissive private key and skips it silently as far as the server is concerned:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
If the failure is on a server you are logging in to, the same rule applies at the far end — ~/.ssh must be 700 and ~/.ssh/authorized_keys must be 600, both owned by the logging-in user. sshd ignores group- or world-writable files, and the client-side symptom is identical to having no key.
Fix 5: Point SSH at the Right Key Explicitly
If you use a non-default filename, or several keys for different hosts, tell SSH which to use in ~/.ssh/config:
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
IdentitiesOnly yes matters more than it looks. Without it, SSH offers every key it knows about, and some servers close the connection after a handful of failed attempts — so the right key never gets tried. That produces Permission denied (publickey) even though the correct key is sitting right there.
Verify the Fix
Re-run the authentication test:
ssh -T git@github.com
If it still fails, run it verbose and read which keys were actually offered:
ssh -vT git@github.com
Look for lines like:
debug1: Offering public key: /Users/you/.ssh/id_ed25519 ED25519 SHA256:abc123...
debug1: Authentications that can continue: publickey
If your key never appears in an Offering public key line, the problem is Fix 3 or Fix 5 — the client is not presenting it. If it is offered and the server still refuses, the problem is Fix 2 — that key is not registered on the account.
Once ssh -T succeeds, confirm your remote is actually using SSH:
git remote -v
An SSH remote looks like git@github.com:user/repo.git. If yours starts with https://, Git was never using SSH, and your key setup was not the problem. Switch it if you want SSH:
git remote set-url origin git@github.com:user/repo.git
Prevention
- Use one key per machine, not one key copied across machines. Revoking a lost laptop then means deleting one key, not rotating everything.
- Add
IdentitiesOnly yesto each~/.ssh/confighost block once you have more than one key. - On macOS, use
--apple-use-keychainso a reboot never costs you an hour. - Back up
~/.ssh/config, not your private keys. Keys are cheap to regenerate; a working config is the part worth keeping.
If you need to work around SSH entirely on a machine you do not control, switching the remote to HTTPS and authenticating with a personal access token is a legitimate fallback — it moves the credential from a key file to a token, with the same access.