git@github.com: Permission denied (publickey) - 12 Causes and Fixes
git@github.com: Permission denied (publickey) is GitHub's SSH server telling you it finished the handshake and your client never presented a key it would accept. The message is not about repository permissions, not about your password, and not about the repo existing — authentication failed before GitHub ever looked at which repository you asked for.
That single fact kills most of the guesswork. Either your client offered no key at all, or it offered a key GitHub does not have on file for an account with access.
This page walks the 12 causes that produce this exact string, each with a reproduction you can run, the lines that change in the fix, and a command that proves it worked. If your case matches none of them, the diagnostic checklist narrows an unknown occurrence down to one of them in about six commands.
| Error | git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.
|
|---|---|
| Where it happens | Git over SSH (OpenSSH client) against github.com - any OS, any Git version, local shells and CI runners alike. Appears on clone, fetch, push, and on a bare ssh -T git@github.com. |
| What it means | Your SSH client presented no public key that GitHub recognises, so GitHub refused the connection before any repository lookup happened. |
The Fast Fix
In most cases your key exists but is not loaded in the ssh-agent. Start the agent, add the key, and retry:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
ssh -T git@github.com
A successful result prints Hi <username>! You've successfully authenticated, but GitHub does not provide shell access. If ssh-add says No such file or directory, you have no key yet — generate one with ssh-keygen -t ed25519 -C "you@example.com" and paste ~/.ssh/id_ed25519.pub into GitHub under Settings → SSH and GPG keys → New SSH key.
If both of those are already true, the key you own is not the key GitHub has. Run the diagnostic checklist below.
What Is Actually Causing It
Jump to your case
- 1. Private key is not loaded into the ssh-agent
- 2. No SSH key pair exists on this machine
- 3. Public key was never added to the GitHub account
- 4. Key lives at a non-default path, so ssh never offers it
- 5. Private key file permissions are too open
- 6. Windows ssh-agent service is stopped or the wrong ssh is used
- 7. Two GitHub accounts, and the wrong key is offered first
- 8. Running git as root or another user, so ~/.ssh differs
- 9. IdentityFile points at the .pub file instead of the private key
- 10. Key was uploaded as a signing key, not an authentication key
- 11. CI job has no key loaded in the runner
- 12. Deploy key is registered on a different repository
1. Private key is not loaded into the ssh-agent
Reproduce it
$ ssh-add -l
The agent has no identities.
$ git push origin main
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.
Why it happens — A passphrase-protected key cannot be used non-interactively unless an agent holds the decrypted copy. With an empty agent and no usable identity on disk, the client reaches the end of its identity list and GitHub closes the connection with publickey as the only method it offered.
The fix
# start an agent in this shell if SSH_AUTH_SOCK is unset
eval "$(ssh-agent -s)"
# load the key (prompts once for the passphrase)
ssh-add ~/.ssh/id_ed25519
What changed: the agent now holds the decrypted private key, so the client has an identity to offer.
Confirm it worked — ssh-add -l lists a fingerprint instead of The agent has no identities., and ssh -T git@github.com greets you by username.
2. No SSH key pair exists on this machine
Reproduce it
$ ls -l ~/.ssh/
total 4
-rw-r--r-- 1 you you 142 Oct 2 11:04 known_hosts
$ git clone git@github.com:yourname/yourrepo.git
git@github.com: Permission denied (publickey).
Why it happens — The client walks its default identity filenames (id_ed25519, id_rsa, and friends), finds none, and has nothing to send. This is the normal state of a fresh machine, a new container, or a reinstalled OS.
The fix
ssh-keygen -t ed25519 -C "you@example.com"
# accept the default path ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub
# paste this single line into GitHub: Settings > SSH and GPG keys > New SSH key
What changed: a key pair now exists at a default filename, and its public half is registered on your account.
Confirm it worked — ls ~/.ssh/id_ed25519* shows both the private and .pub files, and ssh -T git@github.com authenticates.
3. Public key was never added to the GitHub account
Reproduce it
$ ssh-add -l
256 SHA256:yWvVvK1rO2nQ8o5xI3dT7b9pL0mXq4sU6zR1cH2gE8A you@example.com (ED25519)
$ ssh -T git@github.com
git@github.com: Permission denied (publickey).
Why it happens — The agent offers a perfectly valid key, but GitHub has no record of it, so it rejects every identity in turn. A loaded key proves nothing about whether the server side knows it.
The fix
# print the local fingerprint and compare it against the list in
# GitHub > Settings > SSH and GPG keys
ssh-keygen -lf ~/.ssh/id_ed25519.pub
# if it is absent, copy the public key and add it there
cat ~/.ssh/id_ed25519.pub
What changed: the fingerprint shown locally now appears in your account's authentication keys list.
Confirm it worked — The SHA256:... string from ssh-keygen -lf matches a key listed in GitHub's SSH keys page, and ssh -T git@github.com returns the Hi <username>! greeting.
4. Key lives at a non-default path, so ssh never offers it
Reproduce it
$ ls ~/.ssh/
work_github work_github.pub known_hosts
$ ssh -T git@github.com
git@github.com: Permission denied (publickey).
Why it happens — OpenSSH only tries its built-in default filenames unless an IdentityFile tells it otherwise. A key named anything else is invisible to the client even though it sits right there in ~/.ssh.
The fix
# ~/.ssh/config
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/work_github
IdentitiesOnly yes
What changed: IdentityFile points at the real key, and IdentitiesOnly yes stops the agent from drowning it in unrelated keys.
Confirm it worked — ssh -vT git@github.com 2>&1 | grep "Offering public key" shows work_github being offered, followed by the successful greeting.
5. Private key file permissions are too open
Reproduce it
$ chmod 644 ~/.ssh/id_ed25519
$ ssh -T git@github.com
WARNING: UNPROTECTED PRIVATE KEY FILE!
Permissions 0644 for '/home/you/.ssh/id_ed25519' are too open.
This private key will be ignored.
git@github.com: Permission denied (publickey).
Why it happens — OpenSSH refuses to load a private key that other users on the machine can read. It ignores the key rather than failing loudly, so the run continues with no identity and ends in the generic publickey denial. This bites hardest after copying keys out of a backup, a zip, or a Windows filesystem.
The fix
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
What changed: the private key is now readable only by its owner, which is the condition OpenSSH enforces.
Confirm it worked — ssh -vT git@github.com no longer prints UNPROTECTED PRIVATE KEY FILE and reaches Authentication succeeded.
6. Windows ssh-agent service is stopped or the wrong ssh is used
Reproduce it
PS> ssh-add -l
Error connecting to agent: No such file or directory
PS> git push origin main
git@github.com: Permission denied (publickey).
Why it happens — Windows ships the OpenSSH agent as a service that is not started by default, and Git for Windows bundles its own ssh.exe that talks to a different agent than C:\Windows\System32\OpenSSH\ssh.exe. Keys loaded into one agent are unreachable from the other binary.
The fix
# start the Windows OpenSSH agent and keep it running across reboots
Set-Service ssh-agent -StartupType Automatic
Start-Service ssh-agent
ssh-add $env:USERPROFILE\.ssh\id_ed25519
# point Git at the same ssh binary that owns that agent
git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"
What changed: one agent is running, and Git and ssh-add now use the same OpenSSH binary.
Confirm it worked — Get-Service ssh-agent shows Running, ssh-add -l lists your key, and git ls-remote git@github.com:yourname/yourrepo.git returns refs.
More causes (6 remaining)
7. Two GitHub accounts, and the wrong key is offered first
Reproduce it
$ ssh-add -l
256 SHA256:aaa... personal (ED25519)
256 SHA256:bbb... work (ED25519)
$ git clone git@github.com:work-org/internal-repo.git
git@github.com: Permission denied (publickey).
Why it happens — GitHub maps a key to exactly one account. The client offers identities in order, GitHub accepts the first one it recognises — your personal account — and that account has no access, so the attempt is rejected. Because a key cannot be registered on two accounts, offering more keys afterwards does not help.
The fix
# ~/.ssh/config
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/work
IdentitiesOnly yes
# clone through the alias, not through github.com
git clone git@github-work:work-org/internal-repo.git
# for an existing clone, rewrite the remote
git remote set-url origin git@github-work:work-org/internal-repo.git
What changed: the host alias forces one specific key per account instead of letting the agent choose.
Confirm it worked — git remote -v shows the alias, and ssh -T git@github-work greets you as the work username.
8. Running git as root or another user, so ~/.ssh differs
Reproduce it
$ sudo git clone git@github.com:yourname/yourrepo.git /opt/app
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.
Why it happens — sudo resets HOME to /root and drops SSH_AUTH_SOCK, so the client looks for keys in /root/.ssh and finds an empty directory. The same thing happens inside su, in systemd units, and in Docker steps that switch users.
The fix
# clone as yourself, then move or chown the result
git clone git@github.com:yourname/yourrepo.git /tmp/app
sudo mv /tmp/app /opt/app
sudo chown -R appuser:appuser /opt/app
What changed: the clone runs under the account that actually owns the key, instead of root.
Confirm it worked — sudo -i -- sh -c 'echo $HOME; ls ~/.ssh' shows that root's .ssh is empty, confirming the identity mismatch; the unprivileged clone succeeds.
9. IdentityFile points at the .pub file instead of the private key
Reproduce it
# ~/.ssh/config - wrong
Host github.com
IdentityFile ~/.ssh/id_ed25519.pub
$ ssh -T git@github.com
git@github.com: Permission denied (publickey).
Why it happens — Without an agent holding the matching private key, a public key alone cannot produce the signature the server demands. OpenSSH can offer the public half, but it has nothing to sign the challenge with, so the attempt fails at the signature step.
The fix
# ~/.ssh/config - fixed
Host github.com
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
What changed: IdentityFile names the private key; OpenSSH derives the public half from it automatically.
Confirm it worked — ssh-keygen -y -f ~/.ssh/id_ed25519 prints a public key line without error, proving the path in your config is a real private key.
10. Key was uploaded as a signing key, not an authentication key
Reproduce it
$ ssh-add -l
256 SHA256:ccc... you@example.com (ED25519)
$ ssh -T git@github.com
git@github.com: Permission denied (publickey).
# the same fingerprint is visible in GitHub - under "SSH signing keys"
Why it happens — GitHub keeps two separate lists on the SSH keys page: authentication keys and signing keys. A key in the signing list can verify commit signatures but grants no transport access, which is why the fingerprint appears to be "on GitHub" while SSH still refuses.
The fix
# re-add the same public key, choosing key type "Authentication Key"
cat ~/.ssh/id_ed25519.pub
# GitHub > Settings > SSH and GPG keys > New SSH key > Key type: Authentication Key
What changed: the key now exists in the authentication list; the same key may stay in the signing list too.
Confirm it worked — The key appears under the Authentication keys heading on GitHub, and ssh -T git@github.com returns the greeting.
11. CI job has no key loaded in the runner
Reproduce it
# .github/workflows/deploy.yml - fails
- run: git clone git@github.com:yourorg/private-dep.git
git@github.com: Permission denied (publickey).
Error: Process completed with exit code 128.
Why it happens — A CI runner is a fresh machine with no ~/.ssh and no agent. Nothing from your laptop is carried into it, and the default checkout token is an HTTPS credential that SSH never sees.
The fix
- name: Load deploy key
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: |
mkdir -p ~/.ssh
printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/ci_key
chmod 600 ~/.ssh/ci_key
ssh-keyscan github.com >> ~/.ssh/known_hosts
echo "GIT_SSH_COMMAND=ssh -i ~/.ssh/ci_key -o IdentitiesOnly=yes" >> "$GITHUB_ENV"
- run: git clone git@github.com:yourorg/private-dep.git
What changed: the private key arrives from a secret, gets owner-only permissions, and GIT_SSH_COMMAND forces Git to use it.
Confirm it worked — Add ssh -T git@github.com || true as a step; the log shows the authenticated greeting (or the deploy-key equivalent) instead of Permission denied.
12. Deploy key is registered on a different repository
Reproduce it
# ~/.ssh/ci_key is a deploy key on yourorg/repo-a
$ GIT_SSH_COMMAND="ssh -i ~/.ssh/ci_key -o IdentitiesOnly=yes" \
git clone git@github.com:yourorg/repo-b.git
git@github.com: Permission denied (publickey).
Why it happens — A deploy key is scoped to exactly one repository, and GitHub will not let the same public key be added as a deploy key on a second repo. Presenting it for any other repository fails at authentication, which is why you get publickey rather than a repository-access message.
The fix
# generate a distinct key per repository
ssh-keygen -t ed25519 -f ~/.ssh/repo_b_key -C "deploy repo-b" -N ""
cat ~/.ssh/repo_b_key.pub
# add it under: repo-b > Settings > Deploy keys > Add deploy key
GIT_SSH_COMMAND="ssh -i ~/.ssh/repo_b_key -o IdentitiesOnly=yes" \
git clone git@github.com:yourorg/repo-b.git
What changed: each repository gets its own deploy key. If one credential must reach several repos, use a machine user account or a GitHub App token instead.
Confirm it worked — The new key's fingerprint from ssh-keygen -lf ~/.ssh/repo_b_key.pub appears in repo-b's Deploy keys list, and the clone completes.
## None of Those? Narrow It Down
- Run
ssh -T git@github.com. If this fails too, the problem is authentication and has nothing to do with your repository, branch, or remote URL - keep going down this list. If it succeeds butgit pushfails, your remote URL or repository access is the issue, not your key. - Run
ssh-add -l.The agent has no identities.points at cause 1;Error connecting to agentpoints at cause 1 on Unix or cause 6 on Windows; a listed fingerprint rules both out and sends you to step 4. - Run
ls -l ~/.ssh/. No private key file at all confirms cause 2. A key under a non-default name points at cause 4. Permissions looser than-rw-------on the private key point at cause 5. - Run
ssh -vT git@github.comand read the lines starting withOffering public key:. Zero offers means the client never found a usable identity (causes 1, 2, 4, 5, 9). Offers that are all followed by rejection means GitHub does not accept any key you hold (causes 3, 7, 10, 12). - Compare
ssh-keygen -lf ~/.ssh/id_ed25519.pubagainst the fingerprints on GitHub's SSH keys page. Absent entirely confirms cause 3. Present but under theSSH signing keysheading confirms cause 10. - Check which identity GitHub thinks you are: if
ssh -T git@github.comgreets you as the wrong username, your key is bound to the wrong account - cause 7. Fix it with a host alias rather than by shuffling keys in the agent. - Confirm which binary and which user are running.
echo $HOME,whoami, andgit config --get core.sshCommandexpose thesudo/root mismatch (cause 8) and the Windows binary mismatch (cause 6). In CI, echo the same values in a step - runners are a different machine entirely (cause 11). - If your key file begins with
PuTTY-User-Key-File, OpenSSH cannot read it at all. Convert it withputtygen mykey.ppk -O private-openssh-new -o ~/.ssh/id_ed25519, thenchmod 600the result and retry from step 2.
Why This Error Exists At All
SSH public-key authentication is deliberately one-sided and silent. The client proposes identities; the server says yes or no and explains nothing. GitHub never tells you which key it wanted, whether a key was close, or which account a rejected key belongs to - because any of that would be a free oracle for an attacker enumerating valid credentials. The (publickey) in parentheses is not a diagnosis, it is simply the list of authentication methods the server was willing to try. You failed all of them, and there was only one.
That design forces every useful clue to live on your side of the connection, which is why ssh -vT git@github.com is the real debugger here. The verbose log shows the client's full decision trail: which config stanzas matched, which identity files it considered, which keys it actually offered, and where the server cut it off. Almost every cause on this page is distinguishable by a single line in that output - Permissions 0644 ... are too open, no such identity, Offering public key, Authentications that can continue: publickey. The server's message is uniform by design; the client's log is specific by design.
The second half of the design is identity-to-principal mapping. GitHub binds each public key to exactly one principal - a user account, or a single repository in the case of a deploy key - and that binding is unique across the platform. A great deal of confusion about this error comes from assuming keys are like passwords, reusable wherever you have permission. They are not: a key is who you are, not what you may do. Once you internalise that, the whole family of SSH failures becomes readable. Permission denied (publickey) means "I could not establish who you are." ERROR: Repository not found. means "I know who you are, and that person cannot see this repo." Host key verification failed. means "I cannot establish who the server is." Three different stages, three different messages, and the stage tells you which half of the system to go fix.
Stop It From Coming Back
- Commit a per-host
~/.ssh/configwithIdentityFileandIdentitiesOnly yesfor every GitHub account or alias you use, and keep it in your dotfiles repo. This eliminates cause 4 and cause 7 permanently, and keeps the agent from offering keys in arbitrary order. - Add
AddKeysToAgent yesunderHost *in~/.ssh/configso the first successful use of a key loads it into the agent for the rest of the session instead of prompting every time. On macOS addUseKeychain yesalongside it. - Make the agent start with your shell rather than by hand: a guarded
ssh-agentblock in~/.zshrc/~/.bashrcon Unix, orSet-Service ssh-agent -StartupType Automaticonce on Windows. Hand-started agents are the single biggest source of "it worked yesterday". - In CI, write the key from a secret,
chmod 600it, seedknown_hostswithssh-keyscan github.com, and exportGIT_SSH_COMMANDwith-o IdentitiesOnly=yes. Never rely on an inherited agent in a runner, and never echo the key into the log. - Use one deploy key per repository, or a GitHub App installation token when several repositories need the same credential. Reusing a deploy key across repos is rejected by GitHub by design, so it will always surface as this error later.
- Add a
make doctortarget (or equivalent) to your repo that runsssh -T git@github.com,ssh-add -l, andgit remote -vand prints the results. New contributors hit this error on day one, and a scripted check turns a 40-minute onboarding stall into one command.
Errors You Will Probably Hit Next
Host key verification failed.ERROR: Repository not found.sign_and_send_pubkey: signing failed for ED25519 "..." from agent: agent refused operation
Read the parentheses as a stage, not a cause: (publickey) means authentication never completed, so stop checking repository permissions and remote URLs. Work outward in one direction - does an identity exist on disk, is it loaded in the agent, is it offered on the wire, does GitHub have it bound to an account with access - and ssh -vT git@github.com answers every one of those in a single run. If GitHub greets you by the wrong username, you have an identity problem, not a permissions problem.
댓글
댓글 쓰기