Most people meet SSH the same way: a tutorial tells you to paste four commands, one of them asks for a passphrase you weren’t expecting, and eventually git push stops asking for a password. It works, you move on, and you never quite find out what you set up.
I went through this properly on a Windows machine recently and wrote down not just the steps but the reasoning behind each one. That’s what this post is. Three questions, in order: what SSH authentication actually is, why it’s the right default for GitHub, and how to set it up on Windows in both PowerShell and Git Bash.
If you only want the commands, the How section stands alone. But the Why is the part that makes the commands stick.
Table of contents
Open Table of contents
-
Why: three reasons, only one of which is convenience
- 1. Nothing secret crosses the network
- 2. You stop typing credentials
- 3. Revocation is a single click
- Why
ed25519and notrsa - Why the passphrase question is a real decision
- Why
ssh-agentandssh-addexist - Why the
-Ccomment matters less than you’d think - Why your commit email is the one that’s actually public
- Why SSH auth and commit identity are unrelated
What: a key pair, and what each half does
SSH authentication replaces “prove you’re you with a password” with “prove you’re you with a key you already hold.”
When you run ssh-keygen, you produce two files that are mathematically linked:
id_ed25519— the private key. This never leaves your machine. Ever. Anyone who has it can authenticate as you.id_ed25519.pub— the public key. This is the one you paste into GitHub. It’s safe to share; that’s the whole point of it.
The handshake works like this: GitHub holds your public key. When you connect, it sends a challenge that can only be answered by someone holding the matching private key. Your machine answers it locally. The private key itself is never transmitted. That’s the fundamental difference from a password, which has to travel to the server to be checked.
On Windows, these files live in:
C:\Users\<YourUsername>\.ssh
That location is tied to your Windows user profile, not to a drive. This trips people up: if your repos live on D:\Projects and your keys live on C:, nothing needs configuring. Git looks for keys in your home directory regardless of where the repository sits. The two are entirely independent.
You’ll also see ~ used throughout. It’s shorthand for “my home directory” in Git Bash, Linux, and macOS — and PowerShell understands it too, resolving to $env:USERPROFILE. So ~/.ssh and C:\Users\you\.ssh are the same folder written two ways.
Why: three reasons, only one of which is convenience
1. Nothing secret crosses the network
With HTTPS, you authenticate with a personal access token that gets sent to GitHub on every operation. With SSH, your private key stays on disk and only a signed response goes over the wire. There’s no credential in transit to intercept.
2. You stop typing credentials
This is the reason most people actually make the switch, and it’s a legitimate one. Once the key is registered, git clone, push, and pull just work. No token to paste, no token to rotate every ninety days, no credential helper quietly caching something you’ve forgotten about.
3. Revocation is a single click
If a laptop is lost or a machine is decommissioned, you delete that one public key from your GitHub settings. Every other machine you own keeps working. Compare that to a leaked token, which usually means rotating and re-pasting across everything.
The natural extension of this: one key per machine, not one key copied between machines. A key that exists on three laptops can only be revoked on all three at once. A key that exists on one can be revoked precisely.
Why ed25519 and not rsa
The -t flag on ssh-keygen picks the key type — the underlying algorithm.
ed25519 is the modern default. It’s fast, it has a fixed 256-bit strength, and it needs no size flag because there’s no size to choose. rsa is the older option and needs -b 4096 spelled out to be adequately strong, producing a noticeably longer key. Unless something in your environment specifically requires RSA, use ed25519.
Why the passphrase question is a real decision
When ssh-keygen asks for a passphrase, it’s asking whether to encrypt the private key file on disk.
The argument for setting one: if that file is ever copied off your machine — stolen laptop, malware, a backup that ended up somewhere it shouldn’t — it’s inert without the passphrase. The private key is the single most sensitive file in this setup, and a passphrase is the only thing protecting it at rest.
The argument for skipping it: on a machine with full-disk encryption and a strong login password, the added protection is smaller, and you avoid being prompted during git operations.
The middle path, and the one I’d recommend, is: set a passphrase, then use ssh-agent so you only type it once per session. You get the at-rest protection without the repetition.
One thing worth being clear about: the passphrase is used purely locally, to decrypt your private key when it’s needed. It is never sent to GitHub. GitHub doesn’t know it exists.
Why ssh-agent and ssh-add exist
ssh-agent is a small background process that holds your decrypted private key in memory. ssh-add is how you hand a key to it.
Whether you need it depends on your situation:
| Your setup | Do you need the agent? |
|---|---|
| Default location, no passphrase | No. SSH reads the key straight from disk each time. |
| Default location, with passphrase | For convenience. Without it you’re prompted on every push and pull. With it, once per session. |
| Non-default location | Yes — or set IdentityFile in ~/.ssh/config so SSH knows where to look. |
There’s a meaningful difference between the two shells here. In PowerShell, ssh-agent is a Windows service — start it once, set it to Automatic, and it survives reboots. In Git Bash, eval "$(ssh-agent -s)" starts an agent scoped to that terminal session; close the window and it’s gone. Neither is wrong, they’re just different lifetimes.
Why the -C comment matters less than you’d think
The -C flag attaches a comment to the public key. That’s all it is: a label, stored as trailing text in the .pub file, so that when you’re looking at five keys in your GitHub settings you can tell which machine each one came from.
GitHub never validates it. It isn’t publicly visible. -C "work-laptop" is a perfectly good value.
That said, there’s a related decision that does have public consequences, and it’s easy to conflate the two.
Why your commit email is the one that’s actually public
Your SSH key comment is private. Your git commit email is baked into every commit you push and is visible to anyone who clones the repo. These are separate things that happen to both involve an email address, which is why they get confused.
If your repositories are public — a portfolio, open source, anything a recruiter might browse — you probably don’t want your personal email sitting in the commit log where scrapers can find it. GitHub provides a noreply address for exactly this:
GitHub → Settings → Emails → enable “Keep my email address private” → copy the address shown.
It looks like 123456789+username@users.noreply.github.com. Commits authored with it still link to your GitHub profile, your avatar, and your contribution graph, because GitHub recognises it as yours. You get attribution without publishing a mailbox.
The name is a different call. user.name isn’t validated by anything, but it is what shows up next to every commit. If the history is something you’d point an employer at, a real full name reads better than a handle.
So, for a public-facing setup:
- SSH key comment — any descriptive label. Never public.
- Commit email — GitHub noreply address. Public, so keep it scrapable-but-harmless.
- Commit name — your real full name. Public, and you want it recognisable.
- Actual contact details — your GitHub profile bio or a profile README, which is where someone will look for them anyway.
Why SSH auth and commit identity are unrelated
This is the single most common confusion, so it’s worth stating directly:
- SSH keys answer “who is connecting?” — that’s authentication, and GitHub enforces it.
git config user.emailanswers “who wrote this commit?” — that’s a label, and Git requires it, not GitHub.
Git will refuse to create a commit until user.name and user.email are set, even if you never touch a remote. And nothing stops you from setting your commit email to something you don’t own — Git doesn’t check. The only consequence is that the commit won’t link to any GitHub account.
Two different questions, two different mechanisms.
How: PowerShell
# 1. Confirm OpenSSH is available
Get-Command ssh-keygen
# If it's missing, install it (run PowerShell as Administrator):
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
# 2. Generate the key pair
ssh-keygen -t ed25519 -C "123456789+yourusername@users.noreply.github.com"
# Enter to accept the default path; set a passphrase when prompted
# 3. Verify the two files exist
Get-ChildItem C:\Users\<you>\.ssh
Get-Content C:\Users\<you>\.ssh\id_ed25519.pub
# 4. Start the SSH agent service (Administrator, if it's disabled)
Get-Service ssh-agent
Set-Service -Name ssh-agent -StartupType Automatic
Start-Service ssh-agent
# 5. Load the key into the agent
ssh-add C:\Users\<you>\.ssh\id_ed25519
ssh-add -l # should list a fingerprint
# 6. Copy the PUBLIC key to the clipboard
Get-Content C:\Users\<you>\.ssh\id_ed25519.pub | Set-Clipboard
# 7. GitHub → Settings → SSH and GPG keys → New SSH key → paste → Add SSH key
# 8. Test the connection
ssh -T git@github.com
# Type 'yes' to accept GitHub's host fingerprint on first connect
# 9. Set your commit identity
git config --global user.name "Your Full Name"
git config --global user.email "123456789+yourusername@users.noreply.github.com"
# 10. Prove it end to end
cd D:\Projects
git clone git@github.com:yourusername/your-repo.git
Step 3 is worth not skipping. Confirming that both id_ed25519 and id_ed25519.pub were created — and reading the .pub file so you know what you’re about to paste — takes five seconds and rules out the most common failure, which is pasting the wrong file.
How: Git Bash
Same sequence, different shell. The two genuine differences are the agent (session-scoped, not a service) and the clipboard command.
# 1. Check for keys you may already have
ls -al ~/.ssh
# 2. Generate the key pair
ssh-keygen -t ed25519 -C "123456789+yourusername@users.noreply.github.com"
# Enter to accept the default path; set a passphrase when prompted
# 3. Verify
ls -al ~/.ssh
cat ~/.ssh/id_ed25519.pub
# 4. Start an agent for this session
eval "$(ssh-agent -s)"
# 5. Load the key
ssh-add ~/.ssh/id_ed25519
ssh-add -l
# 6. Copy the PUBLIC key to the clipboard
clip < ~/.ssh/id_ed25519.pub
# 7. GitHub → Settings → SSH and GPG keys → New SSH key → paste → Add SSH key
# 8. Test
ssh -T git@github.com
# 9. Commit identity
git config --global user.name "Your Full Name"
git config --global user.email "123456789+yourusername@users.noreply.github.com"
# 10. Real clone
cd /d/Projects
git clone git@github.com:yourusername/your-repo.git
Step 1 first, always. If ~/.ssh already contains an id_ed25519, you have a key — generating a new one at the same path will happily overwrite it and break every service that trusted the old one.
What success looks like
Hi <username>! You've successfully authenticated, but GitHub does not provide shell access.
That message reads like a failure the first time you see it. It isn’t. The -T flag in ssh -T means don’t allocate a pseudo-terminal — don’t try to open an interactive shell on the far end. We only want to know whether authentication works. GitHub is confirming that it does, and reminding you there’s no shell there to give you. That’s the correct, complete success case.
Verifying an existing setup
If you’re coming back to a machine and want to know what state it’s actually in, this sequence answers every question without changing anything:
ls -al ~/.ssh # do key files exist?
cat ~/.ssh/id_ed25519.pub # which public key, and what comment?
ssh-add -l # is the agent running with a key loaded?
cat ~/.ssh/config # any custom host rules? (missing file is normal)
ssh -T git@github.com # does GitHub actually accept this key?
git config --global user.name # commit identity
git config --global user.email
git config --global --list # everything, if you want the full picture
git remote -v # inside a repo: SSH or HTTPS remote?
| Check | Command | Success looks like |
|---|---|---|
| Key files exist | ls -al ~/.ssh | id_ed25519 and id_ed25519.pub present |
| Public key readable | cat ~/.ssh/id_ed25519.pub | Starts with ssh-ed25519 AAAA... |
| Agent loaded | ssh-add -l | A fingerprint, not “no identities” |
| GitHub auth | ssh -T git@github.com | Hi <user>! You've successfully authenticated... |
| Commit identity | git config --global user.email | Your intended address |
| Clone works | git clone git@github.com:... | No password prompt |
| Remote uses SSH | git remote -v | URL begins git@github.com: |
That last one catches a quiet failure worth knowing about. You can set SSH up perfectly and still be pushing over HTTPS, because a repo cloned before the switch keeps its original remote URL. If git remote -v shows https://github.com/..., point it at SSH:
git remote set-url origin git@github.com:yourusername/your-repo.git
What those other files in ~/.ssh are
Two files appear on their own and cause a moment of “did I do something wrong?”
known_hosts stores the fingerprint of every server you’ve connected to and accepted. It’s written the first time you connect — that’s the yes you typed at step 8. On every subsequent connection, SSH checks the server against this record. If GitHub’s host key ever didn’t match, SSH would refuse to connect and warn you loudly, because the most likely explanation is that something is impersonating GitHub. It’s active man-in-the-middle protection, and it’s the reason that first-connect prompt exists rather than being silently auto-accepted.
known_hosts.old is just an automatic backup, created whenever known_hosts gets rewritten — for example by ssh-keygen -R github.com to clear a stale entry. SSH never reads it. Deleting it is harmless.
Common misconceptions
“The passphrase is my GitHub password.” It isn’t related to GitHub at all. It decrypts your private key locally and never leaves your machine.
“I should back up my private key somewhere safe.” Generate a new key per machine instead. Copying private keys around multiplies the number of places one can leak from and makes revocation all-or-nothing.
“My repos are on D:, so I need to configure something.” No. Keys live with your Windows user profile; repo location is irrelevant.
“SSH is set up, so my commits are authenticated as me.” SSH authenticates the connection. The commit author is whatever git config user.email says, and Git doesn’t verify it. Different layer entirely. (If you want commits themselves to be cryptographically verified, that’s commit signing — a separate setup.)
”does not provide shell access means it failed.” It means it worked. Read the first half of the sentence.
“The -C email needs to be real.” It’s a comment. -C "desktop-2026" is fine. The email that actually matters publicly is git config user.email.
Key takeaways
- SSH replaces a transmitted secret with a locally held one — the private key never crosses the network.
- One key per machine. It makes revocation surgical instead of catastrophic.
ed25519is the current default; RSA needs-b 4096to keep up and buys you nothing.- A passphrase protects the private key at rest;
ssh-agentmeans you only type it once per session. Use both. - The
-Ccomment is private and cosmetic.git config user.emailis public and permanent. Don’t confuse them. - SSH keys answer “who is connecting”; commit identity answers “who wrote this.” Two unrelated mechanisms.
ssh -T git@github.comis the one command that tells you whether it genuinely works.
FAQ
Do I have to use the SSH agent?
Only if your key has a passphrase or lives outside the default location. A passphrase-free key at ~/.ssh/id_ed25519 is read straight from disk. But a passphrase plus an agent is a better trade than skipping both.
Can I use the same key on my work and personal machines? You can, but don’t. Separate keys mean you can revoke one machine without disturbing the other, and your GitHub key list tells you which machines have access.
Why doesn’t my agent survive closing Git Bash?
Because eval "$(ssh-agent -s)" starts an agent bound to that shell session. The Windows ssh-agent service, set to Automatic in PowerShell, persists across reboots. If you want persistence in Git Bash, start the agent from your ~/.bashrc or use the Windows service.
HTTPS with a personal access token works fine. Is SSH really better? For day-to-day work, mostly it’s the same experience with fewer prompts and no token expiry to manage. The real advantages are that no credential is transmitted and revocation is per-machine. In an environment where tokens get pasted into config files and forgotten, that difference matters.
I set everything up but git still asks for a password.
Almost always an HTTPS remote left over from an earlier clone. Check git remote -v and switch it with git remote set-url.
Does the commit email have to match my GitHub account? Only if you want commits to link to your profile and count toward your contribution graph. The noreply address counts. An unrecognised address produces valid commits that simply aren’t attributed to anyone.
Closing thought
None of this is difficult, but almost all of it is opaque the first time. The commands are four minutes of work; understanding what you’ve built is what stops you from being stuck the next time something doesn’t connect.
The shape worth carrying away: a secret that stays on your machine, a public half you can hand out freely and revoke individually, a fingerprint record that catches impersonation, and — completely separately — a name and email that label your work. Once those four ideas are distinct in your head, the commands stop being incantations and start being obvious.