Git Authentication
Git operations on the estate authenticate to GitHub through one of two mechanisms:
an SSH key for repository transport (clone, push, pull), and a fine-grained
personal access token for the GitHub API (the gh CLI, automation). This page
explains both, the rules for choosing between them, and how to verify
authentication on any machine.
Mechanism overview
Section titled “Mechanism overview”Authentication answers two different questions:
- Transport — who may clone/push/pull a repository over the wire?
- API — which operations may scripts perform through the GitHub REST API (opening pull requests, triaging issues, reading repository metadata)?
The two answers are deliberately different mechanisms:
| Question | Mechanism | Where it lives | Expiry |
|---|---|---|---|
| Transport | SSH key pair | ~/.ssh/id_ed25519 + the public key registered on the GitHub account |
none |
| API | Fine-grained personal access token (PAT) | gh CLI (~/.config/gh/hosts.yml) or an environment variable |
optional, set at creation |
SSH key — the transport mechanism
Section titled “SSH key — the transport mechanism”An SSH key pair is a private key (~/.ssh/id_ed25519) and a public key
(~/.ssh/id_ed25519.pub). The public key is registered on the GitHub account
(Settings → SSH and GPG keys); the private key stays on the machine. During a
git push, the client proves possession of the private key; GitHub never sees
the key material.
Why SSH is the standard for transport:
- No expiry — a key works until it is removed from the account.
- No secret in the repository — the remote URL is
git@github.com:<owner>/<repo>.git; authentication happens out of band. - One credential per machine — every repository on the same host uses the same key.
Verify a key:
ssh -T git@github.com# Hi <user>! You've successfully authenticated, but GitHub does not provide shell access.Fine-grained token — the API mechanism
Section titled “Fine-grained token — the API mechanism”A fine-grained personal access token (Settings → Developer settings →
Fine-grained tokens) grants scoped, per-repository permissions (for example
Contents: Read and write on a single repository) and can carry an expiry
date. It is the recommended token kind for the API; classic tokens are
deprecated for new use.
Fine-grained tokens are valid for:
- the
ghCLI (gh auth login --with-token), - API calls in automation (
GH_TOKEN/GITHUB_TOKEN), - HTTPS git transport as a fallback (through a credential helper).
They are NOT meant to be embedded in remote URLs — see below.
Verify a token only when it is needed, and never print it:
GH_TOKEN="$TOKEN" gh api user --jq .login # prints the account loginWhat belongs where
Section titled “What belongs where”| Use case | Mechanism | Example |
|---|---|---|
| Clone / push / pull | SSH key | git clone git@github.com:owner/repo.git |
| gh CLI (PRs, issues, review) | Fine-grained PAT | gh pr view 12 |
| Automation / CI scripts | Fine-grained PAT | GH_TOKEN=... gh api ... |
| HTTPS fallback transport | PAT through credential helper | gh auth setup-git |
The rule: credentials never belong in a remote URL
Section titled “The rule: credentials never belong in a remote URL”Embedding a token in a clone URL (https://x-access-token:<TOKEN.....)
works at first, then leaks the secret through:
git remote -vandgit config --liston the machine,- shell history and terminal logs,
- backup archives and error reports,
- any repository or CI log where the URL is echoed.
A leaked token is compromised even if the repository itself is private: revoke it immediately in Developer settings and never re-embed it. Convert an existing token-in-URL remote with:
git remote set-url origin git@github.com:<owner>/<repo>.gitgit remote -v # verify: no token visibleVerification on this estate
Section titled “Verification on this estate”| Check | Command | Expected |
|---|---|---|
| SSH key | ssh -T git@github.com |
Hi <user>! You've successfully authenticated |
| Transport, any repo | git ls-remote origin HEAD (in a repo) |
exit 0, one line <sha> HEAD |
| No token in URLs | git remote -v |
every remote starts with git@ or plain https:// |
| gh API | gh api user --jq .login |
the account login |
| HTTPS fallback | git ls-remote https://github.com/<owner>/<repo>.git HEAD |
exit 0 |