Terminal, Git & GitHub
Terminal, Git & GitHub

SSH keys, tokens & the GitHub CLI

Lesson · ~30 min
After this unit you can
  • Tell an HTTPS remote from an SSH remote by its URL, say what each authenticates with, and switch a clone with git remote set-url
  • Create an Ed25519 key with a passphrase, let the agent and Keychain hold it, and give GitHub only the .pub half
  • Read ssh -T and ssh -G output (exit 1 is success) and order ~/.ssh/config so a second account's Host alias offers the right key
  • Choose a fine-grained token, gh auth login or GH_TOKEN for a person vs a script, and explain why logins and passphrases belong in your own terminal
  • Run the clone → branch → PR → review loop with gh repo clone, gh pr create, gh pr checkout and gh pr view

Read

Simple
1 / 2

Two ways git proves who you are to GitHub

Flag

Cloning a public repo over HTTPS needs no identity. Pushing — or reading a private repo — does (and SSH asks for a key even for public repos). Git talks to GitHub in one of two ways, and the remote URL decides which:

% git remote -v
origin  https://github.com/acme/api.git (fetch)     # HTTPS
origin  git@github.com:acme/api.git (fetch)         # SSH (one or the other, not both)
HTTPSSSH
URLhttps://github.com/acme/api.gitgit@github.com:acme/api.git
You prove identity witha token (typed where git asks for a password)a key pair on your machine
Stored bya credential helper (macOS Keychain)~/.ssh/ + the ssh agent
Typical blockera corporate proxy that allows only HTTPSa network that blocks port 22

Your GitHub password doesn't work for either: GitHub removed password authentication for git operations. Over HTTPS, the "password" git asks for is a personal access token.

The key pair, in one paragraph

ssh-keygen makes two files. id_ed25519 is the private key: it never leaves your machine and is never pasted anywhere. id_ed25519.pub is the public key: you paste that into GitHub (Settings → SSH and GPG keys). When you connect, your machine proves it holds the private key that matches a public key GitHub has on file — and GitHub knows which account that public key belongs to. That's how it decides who you are.

% cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA… ada@example.com      # safe to share

Switching an existing clone

The protocol isn't baked into the repo, only into the remote's URL:

git remote set-url origin git@github.com:acme/api.git     # HTTPS → SSH
git remote -v                                             # check

Nothing else changes — same branches, same history. Pick one protocol and use it everywhere; mixing them is how you end up typing a token for one repo and a passphrase for the next.

Medium
1 / 2

An SSH key on macOS, line by line

Flag

GitHub's macOS instructions, with what each step actually does. Run them in your own terminal (the transcripts are OpenSSH 10.2 on macOS 26; older versions word the prompts a little differently).

1. Make the key pair

% ssh-keygen -t ed25519 -C "ada@example.com"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/Users/ada/.ssh/id_ed25519):
Enter passphrase for "/Users/ada/.ssh/id_ed25519" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/ada/.ssh/id_ed25519
Your public key has been saved in /Users/ada/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:… ada@example.com
  • -t ed25519 — the key type GitHub's instructions use (an Ed25519 public key is one short line).
  • -C — a comment stored at the end of the public key. Without it, OpenSSH uses user@hostname, which puts your machine's name into every server you register the key with.
  • The passphrase encrypts the private key on disk. A stolen laptop backup or a leaked dotfiles repo then holds a file that's useless without it. Use one.

ssh-keygen writes the private key readable only by you (-rw-------). Copy keys around by hand and loosen that, and OpenSSH refuses them: WARNING: UNPROTECTED PRIVATE KEY FILE! … Permissions 0644 for '…' are too open. chmod 600 the private key to fix it.

2. Let the agent remember the passphrase

Typing the passphrase on every git push would get old. The ssh-agent holds unlocked keys in memory, and on macOS the Keychain can store the passphrase so the agent unlocks the key for you:

# ~/.ssh/config
Host github.com
  AddKeysToAgent yes
  UseKeychain yes
  IdentityFile ~/.ssh/id_ed25519
ssh-add --apple-use-keychain ~/.ssh/id_ed25519    # asks for the passphrase once, saves it

macOS already runs an agent for your login session (echo $SSH_AUTH_SOCK shows its socket); GitHub's guide also starts one explicitly with eval "$(ssh-agent -s)". UseKeychain and --apple-use-keychain are Apple additions — a non-Apple OpenSSH (Homebrew's, say) stops at Bad configuration option: usekeychain, which is what GitHub's IgnoreUnknown UseKeychain workaround is for. (Before macOS 12, the flag was -K.)

3. Give GitHub the public half, and test

Paste the contents of ~/.ssh/id_ed25519.pub into GitHub → Settings → SSH and GPG keys (or gh ssh-key add ~/.ssh/id_ed25519.pub). Then:

% ssh -T git@github.com
Hi ada! You've successfully authenticated, but GitHub does not provide shell access.
% echo $?
1

That is success. -T asks for no terminal, GitHub refuses to give you a shell, and the command exits 1 — GitHub's docs say to expect exactly that. The line that matters is the username: it's the account your key belongs to.

Failure looks different:

git@github.com: Permission denied (publickey).        # exit 255 — ssh itself failed

That means GitHub accepted none of the keys offered — usually the public key was never added, or ssh offered a different key file than the one you registered.

Finally, point existing clones at SSH: git remote set-url origin git@github.com:acme/api.git.

Complex
1 / 2

~/.ssh/config — first value wins, and two GitHub accounts

Flag

GitHub identifies you by which key authenticates. Two accounts on one laptop (personal and work) therefore means two keys, and a way to make ssh offer the right one. The tool is ~/.ssh/config — and its one rule surprises everyone who expects "later lines override".

The rule

The ssh_config(5) manual: "for each configuration directive, the first specified value will be used", so "more host-specific declarations should be given near the beginning of the file, and general defaults at the end." Every Host block whose pattern matches contributes, top to bottom; a later block can't change a value an earlier one already set. IdentityFile is the exception that accumulates: every matching one is added to the list, in file order, and ssh tries them in that order.

ssh -G <host> prints the configuration ssh would use, without connecting. With -F it reads only the file you give it:

# config-bad: defaults first
Host *
  User ada
  IdentityFile ~/.ssh/id_ed25519

Host github-work
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519_work
  IdentitiesOnly yes
% ssh -G -F config-bad github-work | grep -E '^(user|hostname|identityfile) '
user ada
hostname github.com
identityfile ~/.ssh/id_ed25519
identityfile ~/.ssh/id_ed25519_work

User git lost to the earlier User ada, and the personal key is offered first. Move the github-work block above Host * and the same command prints user git and the work key first. (A git URL like git@github-work:… names the user itself, so the User line doesn't bite there — the key order does.)

Two accounts that actually work

# ~/.ssh/config — specific first, defaults last
Host github-work
  HostName github.com
  IdentityFile ~/.ssh/id_ed25519_work
  IdentitiesOnly yes

Host github.com
  IdentityFile ~/.ssh/id_ed25519
  IdentitiesOnly yes
git clone git@github-work:acme/api.git            # the alias picks the work key
git remote set-url origin git@github-work:acme/api.git   # for an existing clone
ssh -T git@github-work                            # Hi ada-work! …

github-work isn't a real host: HostName maps it to github.com, and the alias is just a handle that selects a block. IdentitiesOnly yes matters when the agent holds both keys — without it, ssh may offer agent keys too, and GitHub signs you in with whichever it accepts first. GitHub's "Managing multiple accounts" page shows this same pattern (for a managed enterprise account), plus a git setting, url."git@github-work:acme/".insteadOf "git@github.com:acme/", so URLs copied from the website are rewritten to the alias automatically.

Reading the symptom

ERROR: Permission to acme/api.git denied to ada-personal.
fatal: Could not read from remote repository.

GitHub's docs: this "means the key you are pushing with is attached to an account which does not have access to the repository." Authentication succeeded — as the wrong account. That's a key-order problem, not a permissions one: check with ssh -T against the same host alias the remote uses, and look at the name in the greeting.

Quiz

git remote -v shows origin git@github.com:acme/api.git. When you push, what proves who you are?

Flag

GitHub's "New SSH key" form wants a key. What do you paste?

Flag

ssh -T git@github.com prints Hi ada! You've successfully authenticated, but GitHub does not provide shell access. and echo $? prints 1. Is SSH set up correctly?

Flag

Your ~/.ssh/config starts with Host * → IdentityFile ~/.ssh/id_ed25519 (personal), then Host github-work → HostName github.com, IdentityFile ~/.ssh/id_ed25519_work, IdentitiesOnly yes. Pushing to git@github-work:acme/api.git fails with ERROR: Permission to acme/api.git denied to ada-personal. Why?

Flag

A small script will add labels to issues in acme/api, and nothing else. What credential should it use?

Flag

A CI job needs to run gh pr comment. How should it authenticate?

Flag

A teammate's PR #42 comes from their fork. You want to run it locally. Quickest correct move?

Flag

Fill in the blank

Generate the key type GitHub's instructions use, with your email as the comment.

Flag
ssh-keygen -t  -C "ada@example.com"

On macOS, load the key into the agent automatically and keep its passphrase in the Keychain.

Flag
Host github.com
   yes
  UseKeychain yes
  IdentityFile ~/.ssh/id_ed25519

# once, in your own terminal:
ssh-add  ~/.ssh/id_ed25519

Point an existing HTTPS clone at the SSH URL instead.

Flag
git remote  origin git@github.com:acme/api.git

Give a second account its own alias that connects to GitHub and offers only the work key.

Flag
Host github-work
   github.com
  IdentityFile ~/.ssh/id_ed25519_work
   yes

Open a draft PR from your branch using the commit messages, then check out a teammate's PR #42.

Flag
gh pr  --draft --fill
gh pr  42

Predict the output

% cat config-bad
Host *
  User ada
  IdentityFile ~/.ssh/id_ed25519

Host github-work
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519_work

% ssh -G -F config-bad github-work | grep -E '^(user|identityfile) '
Flag
# run from a script: no terminal attached
% true | ssh-keygen -q -t ed25519 -f ./ci_key -C ci
Enter passphrase for "./ci_key" (empty for no passphrase): Enter same passphrase again:
% echo $?
% ssh-keygen -y -P "" -f ./ci_key > /dev/null && echo "opens with an empty passphrase"
Flag

Further reading