TRENDING
Rows of identical brass-colored apartment mailboxes with small locks and name labels along an orange corridor wall
October 9, 2026
How to Prevent Broken Object Level Authorization (IDOR) in a FastAPI App
Street-level upward view of the Monetary Authority of Singapore building and neighbouring office towers under a pale sky
October 9, 2026
Singapore’s AI Guidelines Turn Independent Review Into a Question of Who Sets the Risk Rating
Cast-iron late Qing dynasty coin minting press with a large flywheel, displayed in a museum case
October 9, 2026
Attackers Hijacked the .gh, .sl and .as Country Domains and Minted HTTPS Certificates for Google
Rows of closed oak library card catalog drawers, each with a brass pull and a blank label holder
October 9, 2026
How to Encrypt PII in Python and Keep It Searchable With Blind Indexes
Close-up of a vintage Western Electric manual telephone switchboard with orange lamps, red patch cords plugged into jacks, a rotary dial and a black handset
October 9, 2026
Microsoft’s Agent Lightning v1.0 Turns Agent Training Into a Sample-Accounting Problem
09 Oct 2026
SXZ.io SXZ.io
  • Home
Search the Site
Popular Searches:
Technology Amazon AI
Recent Posts
Two orange safety relief valves on grey pressure vessels in an industrial plant
How to Add Backpressure and Load Shedding to a Python Service Before Overload Takes It Down
October 8, 2026
Yellow diamond-shaped merging traffic warning sign showing a side road joining a main road
GitHub’s Git Rebuild Turns Repository Durability and Read Scale Into Two Separate Problems
October 8, 2026
A lugworm lying on wet sand and mud at low tide
A Compromised Admin Account Put the Shai-Hulud Worm Into AI Sandbox Maker Tensorlake’s npm SDK
October 8, 2026
SXZ.io SXZ.io
  • Home

Categories

Articles 232 Posts
News 234 Posts
Learning Hub 204 Posts
Home/Learning Hub/How to Connect Git to GitHub With SSH Keys and Open Your First Pull Request
Learning Hub

How to Connect Git to GitHub With SSH Keys and Open Your First Pull Request

A step-by-step tutorial on generating an SSH key, connecting Git to GitHub, and pushing your first repository through a branch and pull request.

August 2, 2026 14 Min Read
51

Git tracks changes to files on your computer. GitHub hosts a copy of that history on a server so you can back it up, share it, and collaborate with other people. The two are often mentioned in the same breath, but they are different tools that meet at exactly one point: the remote, a copy of your repository that both your computer and GitHub’s servers know how to talk to. This tutorial builds that connection from scratch: you will generate an SSH key, prove to GitHub that you own it, create a local project, connect it to a remote, push your first commit, and walk through the branch-and-pull-request workflow that almost every professional team uses to review code before it ships.

Table Of Content

  • Why SSH instead of a password
  • Prerequisites
  • Step 1: Set your Git identity
  • Step 2: Generate an SSH key for GitHub
  • Step 3: Add your public key to GitHub and test the connection
  • Why that message isn’t an error
  • Step 4: Create your first local repository
  • A common first mistake: committing before staging
  • Step 5: Create an empty repository on GitHub and connect it
  • Step 6: Push your first commit
  • Step 7: Create a feature branch and make a change
  • Step 8: Push the branch and open a pull request
  • Why GitHub’s merge button behaves differently than a plain git merge
  • Common mistakes and gotchas
  • Verify everything works end-to-end
  • Next steps

Every command below was run against a real installation of Git 2.53.0 and OpenSSH 10.2 on Ubuntu, and the local Git mechanics (staging, committing, branching, pushing, the divergent-branch and rejected-push scenarios) were executed end to end against a real Git remote to capture genuine output, not invented. The GitHub.com-specific steps, the exact buttons and fields on github.com’s website, are described from GitHub’s own official documentation, fetched directly while writing this tutorial, since a screen you click through isn’t something a terminal history can verify. Both kinds of steps are labeled as you go so you always know which is which.

Why SSH instead of a password

GitHub no longer accepts your plain account password for Git operations like git push, whether you’re pushing over HTTPS or connecting any other way. HTTPS pushes now require a personal access token typed in place of a password; SSH pushes require a key pair instead. This tutorial uses SSH because, once it’s set up, you never type a credential again: your terminal proves who you are automatically for every future git push or git pull, and the private half of the key never leaves your machine.

Prerequisites

  • A Linux or macOS machine with terminal access. The commands are the same on any modern distribution; Windows users should run them inside WSL or Git Bash.
  • Git installed. Check with git --version; if that fails, install it from your package manager (apt install git, brew install git, and so on).
  • OpenSSH’s client tools, ssh and ssh-keygen. These ship by default on macOS and almost every Linux distribution.
  • A free GitHub.com account. Creating one is outside the scope of this tutorial, but it takes a couple of minutes at github.com.
  • No prior Git experience is assumed. Every command is explained the first time it appears.

Step 1: Set your Git identity

Every commit you make is stamped with a name and email address. Git won’t let you commit until it knows what to write there. Set both globally so every repository on your machine uses them by default:

# Context: any terminal with Git installed, run once per machine.
git config --global user.name "Your Name"
git config --global user.email "[email protected]"

Use the same email address you plan to use for your GitHub account; GitHub matches commit emails to accounts to show your avatar next to your commits. Confirm the values stuck:

git config --global user.name
git config --global user.email

These commands print back exactly what you set, with no other output. There’s also a small but genuinely useful setting worth adding while you’re here: modern Git still defaults new repositories to a branch named master unless you tell it otherwise, while GitHub has defaulted new repositories to main since 2020. Setting this avoids a branch-name mismatch later:

git config --global init.defaultBranch main

Step 2: Generate an SSH key for GitHub

An SSH key is a matched pair of files: a private key that stays on your computer and must never be shared, and a public key that’s safe to hand to anyone, including GitHub. GitHub uses the public key to verify that whoever is pushing controls the matching private key, without the private key ever crossing the network. GitHub’s own documentation recommends the Ed25519 key type for new keys, a modern algorithm that’s both smaller and faster to verify than older RSA keys:

# Context: any terminal, generates a new key pair.
ssh-keygen -t ed25519 -C "[email protected]"

Use the same email you configured in Step 1 for the -C comment; it’s just a label embedded in the public key to help you identify it later, not a security requirement. Accept the default file location by pressing Enter, and you’ll be asked for an optional passphrase. A passphrase encrypts the private key file at rest, so if your laptop is ever stolen, the key alone isn’t enough to impersonate you; it’s optional but recommended for anything beyond a throwaway test machine. The output looks like this:

Generating public/private ed25519 key pair.
Your identification has been saved in /home/you/.ssh/id_ed25519
Your public key has been saved in /home/you/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:1qyZmUI34BXXuI03UcboxmlILlgnrT3McnR6rfn3n+A [email protected]
The key's randomart image is:
+--[ED25519 256]--+
|        ...o +o  |
|        oo* =..  |
|      .o.@ X +   |
|     ..o+o% @ .  |
|      o S+o* +   |
|     . o B  o    |
|      . *    o   |
|       .    . o o|
|             E o=|
+----[SHA256]-----+

Your fingerprint and randomart will be different; they’re mathematically derived from your specific key. Two files now exist in ~/.ssh/: id_ed25519 (private, never share this) and id_ed25519.pub (public, safe to share). Start the SSH agent, a small background process that holds your decrypted key in memory so you aren’t retyping your passphrase on every connection, and add your key to it:

# macOS and Linux
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

On macOS specifically, add --apple-use-keychain before the key path so the passphrase is remembered across reboots via the system Keychain.

Step 3: Add your public key to GitHub and test the connection

The steps in this section describe GitHub.com’s web interface, verified against GitHub’s official documentation. Print your public key and copy the entire output, from ssh-ed25519 through your email comment:

cat ~/.ssh/id_ed25519.pub

On GitHub.com: click your profile picture in the top-right corner, choose Settings, then SSH and GPG keys in the sidebar under Access. Click New SSH key, give it a descriptive title (like the name of the machine it’s from, for example “Home laptop”), leave the key type as Authentication Key, paste your public key into the Key field, and click Add SSH key. GitHub may ask you to confirm your password or a two-factor code as a final check.

Back in your terminal, verify the connection actually works before you rely on it:

ssh -T [email protected]

The very first time you connect to any new host, SSH doesn’t yet know GitHub’s server identity and asks you to confirm it:

The authenticity of host 'github.com (IP ADDRESS)' can't be established.
ED25519 key fingerprint is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU.
Are you sure you want to continue connecting (yes/no)?

Type yes. That specific fingerprint is GitHub’s published, documented key fingerprint; SSH remembers it afterward in ~/.ssh/known_hosts and will only ask again if it ever changes, which would be a red flag worth investigating rather than clicking through. A successful authentication looks like this:

Hi your-username! You've successfully authenticated, but GitHub does not provide shell access.

Why that message isn’t an error

New Git users often see “GitHub does not provide shell access” and assume something failed. It didn’t. GitHub’s SSH endpoint only exists to authenticate Git commands, it deliberately refuses to hand you an interactive shell the way a normal SSH server would. Seeing your own username echoed back in that sentence is the actual success signal.

Step 4: Create your first local repository

Make a project directory and turn it into a Git repository:

mkdir server-toolkit
cd server-toolkit
git init
Initialized empty Git repository in /home/you/server-toolkit/.git/

git init creates a hidden .git directory that holds the entire history of the project, every commit, branch, and tag. Nothing is tracked yet. Add a couple of real files:

cat > check_disk_space.py <<'EOF'
#!/usr/bin/env python3
"""Warn when any mounted filesystem is above a usage threshold."""
import shutil
import sys

THRESHOLD_PERCENT = 80

def check_path(path):
    usage = shutil.disk_usage(path)
    percent_used = (usage.used / usage.total) * 100
    return percent_used

if __name__ == "__main__":
    percent = check_path("/")
    print(f"/ is {percent:.1f}% full")
    if percent >= THRESHOLD_PERCENT:
        print(f"WARNING: usage is at or above {THRESHOLD_PERCENT}%")
        sys.exit(1)
    sys.exit(0)
EOF

cat > README.md <<'EOF'
# server-toolkit

Small command-line scripts for basic server health checks.

## Scripts

- `check_disk_space.py`: warns when root filesystem usage crosses 80%.
EOF

A common first mistake: committing before staging

New files don’t get swept into a commit automatically. Try committing right now, before telling Git which files to include:

git commit -m "Add disk space checker"
On branch main

Initial commit

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	README.md
	check_disk_space.py

nothing added to commit but untracked files present (use "git add" to track)

Git refuses, and tells you exactly why: the files exist on disk but haven’t been staged. Staging is a deliberate middle step between “file exists” and “file is part of the next commit,” and it exists so you can build a commit out of specific changes rather than blindly committing everything that happens to be sitting in the directory. Stage the files, then commit:

git add check_disk_space.py README.md
git commit -m "Add disk space checker script"
[main (root-commit) 6252b70] Add disk space checker script
 2 files changed, 26 insertions(+)
 create mode 100644 README.md
 create mode 100644 check_disk_space.py

That 6252b70 is the first seven characters of the commit’s SHA-1 hash, a fingerprint of the entire project state at that point, generated the moment you commit. Confirm it’s there:

git log --oneline
6252b70 Add disk space checker script

Step 5: Create an empty repository on GitHub and connect it

This section describes GitHub.com’s web interface. On github.com, click the + icon in the top-right corner and choose New repository. Enter a repository name, for example server-toolkit to match your local folder, though the names don’t technically have to match. Choose Public or Private; either works for this tutorial and you can change it later in the repository’s settings. This is the one field that matters most for what comes next: leave every “Initialize this repository with” checkbox (README, .gitignore, license) unchecked. You already have a README locally; if GitHub creates its own, the two histories won’t share a common starting point and your first push will be rejected. Click Create repository.

GitHub then shows you a page with setup instructions and a remote URL in SSH form, something like [email protected]:your-username/server-toolkit.git. Back in your terminal, connect your local repository to it:

git remote add origin [email protected]:your-username/server-toolkit.git
git remote -v
origin	[email protected]:your-username/server-toolkit.git (fetch)
origin	[email protected]:your-username/server-toolkit.git (push)

origin is just a conventional name for “the remote I cloned from or primarily push to”; Git doesn’t treat it specially, but almost every tool and tutorial assumes it, so there’s little reason to rename it.

Step 6: Push your first commit

git push -u origin main
To github.com:your-username/server-toolkit.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

The -u flag (short for --set-upstream) links your local main branch to origin/main, so every future plain git push or git pull in this repository knows where to go without you specifying it again. Refresh the repository page on GitHub and your README and script are there. This is also a convenient moment to add a .gitignore file, which tells Git about files it should never stage, generated caches, local secrets, anything that shouldn’t be shared:

cat > .gitignore <<'EOF'
__pycache__/
*.pyc
.env
EOF
git add .gitignore
git commit -m "Add .gitignore for cache files and local secrets"
git push

Notice that second push needed no flags at all; the upstream link from -u is doing its job. A .gitignore only prevents future accidental commits, it does nothing for files you already committed. If a real secret ever ends up in your history, rotate the credential immediately; deleting the file in a new commit still leaves it recoverable from the project’s history.

Step 7: Create a feature branch and make a change

Committing straight to main works alone, but the moment anyone else is involved, teams isolate changes on a branch so main always reflects working, reviewed code. Create one:

git checkout -b add-verbose-flag
Switched to a new branch 'add-verbose-flag'

checkout -b is shorthand for two operations: create a new branch pointing at your current commit, then switch to it. Everything you commit now belongs to add-verbose-flag until you switch again. Make a real change:

cat > check_disk_space.py <<'EOF'
#!/usr/bin/env python3
"""Warn when any mounted filesystem is above a usage threshold."""
import argparse
import shutil
import sys

THRESHOLD_PERCENT = 80

def check_path(path):
    usage = shutil.disk_usage(path)
    percent_used = (usage.used / usage.total) * 100
    return percent_used

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--verbose", action="store_true")
    args = parser.parse_args()
    percent = check_path("/")
    if args.verbose:
        print(f"Checking / against threshold of {THRESHOLD_PERCENT}%")
    print(f"/ is {percent:.1f}% full")
    if percent >= THRESHOLD_PERCENT:
        print(f"WARNING: usage is at or above {THRESHOLD_PERCENT}%")
        sys.exit(1)
    sys.exit(0)
EOF
python3 check_disk_space.py --verbose
Checking / against threshold of 80%
/ is 25.4% full

The script runs and the new flag works. Stage and commit it on the branch:

git add check_disk_space.py
git commit -m "Add --verbose flag to disk space checker"

Step 8: Push the branch and open a pull request

git push -u origin add-verbose-flag
To github.com:your-username/server-toolkit.git
 * [new branch]      add-verbose-flag -> add-verbose-flag
branch 'add-verbose-flag' set up to track 'origin/add-verbose-flag'.

The rest of this step describes GitHub.com’s web interface for creating a pull request. Visit the repository on GitHub and you’ll see a yellow banner offering to compare and open a pull request for the branch you just pushed; click Compare & pull request. Confirm the base branch is main and the compare branch is add-verbose-flag, add a title and description explaining what the change does and why, and click Create pull request. A pull request isn’t a Git concept, it’s a GitHub feature: a page for discussing a proposed set of commits, running automated checks against them, and requesting review, before they become part of main. On a real team, this is where a reviewer leaves comments; on a solo project, it’s still useful as a deliberate checkpoint. When you’re satisfied, click Merge pull request, then Confirm merge.

Why GitHub’s merge button behaves differently than a plain git merge

This is worth understanding, because it’s a real source of confusion. If you merge a branch locally with plain git merge and no other changes have happened on main since you branched, Git takes a shortcut called a fast-forward: it just moves the main pointer forward to your branch’s latest commit, with no new commit created at all. GitHub’s default Merge pull request button does not do this. Per GitHub’s own documentation, it always merges with the equivalent of git merge --no-ff, creating a dedicated merge commit even when a fast-forward was possible, so your repository’s history always shows exactly where a pull request was merged. If you ever merge a branch locally and are surprised that no merge commit appeared, this is why; use git merge --no-ff branch-name to force one.

Back in your terminal, switch to main and pull down the merge GitHub just created:

git checkout main
git pull
From github.com:your-username/server-toolkit
   6252b70..4506b06  main             -> origin/main
Updating 6252b70..4506b06
Fast-forward
 check_disk_space.py | 6 ++++++
 1 file changed, 6 insertions(+)

Note the terminology isn’t contradictory: GitHub’s server-side merge of your pull request created a real merge commit on main; your local main, which had no new commits of its own, then fast-forwards to catch up to that same commit. Both statements are true at once, they’re describing two different repositories (yours and GitHub’s) reaching the same state by different paths.

Common mistakes and gotchas

Push rejected: “fetch first.” This happens when the remote has a commit you don’t have locally, typically because someone else, or you from another machine, pushed in the meantime. For example: you commit a README update locally but don’t push it yet, and while it sits there, a teammate commits and pushes a change of their own straight to main. Your next push hits this:

git push origin main
To github.com:your-username/server-toolkit.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to 'github.com:your-username/server-toolkit.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Git is protecting you from silently overwriting someone else’s work; it will never let a push discard commits it doesn’t know about. The fix is exactly what the hint says, pull first:

git pull

On a reasonably current Git version, since your local branch and the remote branch have genuinely diverged (both have a commit the other doesn’t: your README change, their check_disk_space.py change), a plain git pull refuses to guess how to combine them:

From github.com:your-username/server-toolkit
   4506b06..733d898  main       -> origin/main
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.

For a beginner, the merge strategy is the least surprising choice and matches what Git did by default for years:

git pull --no-rebase
Merge made by the 'ort' strategy.
 check_disk_space.py | 2 ++
 1 file changed, 2 insertions(+)

Because your change and your teammate’s change touched different lines, Git combined them automatically into a new merge commit and didn’t ask you to resolve anything by hand. Now the push succeeds normally:

git push origin main
To github.com:your-username/server-toolkit.git
   733d898..4c0e1bf  main -> main

If you’d rather not decide the reconciliation strategy every time, set a permanent default with git config --global pull.rebase false. And if your change and theirs had touched the same lines of the same file, Git would have stopped mid-merge and marked the file with conflict markers for you to resolve by hand instead of merging automatically, a real merge conflict, which is a big enough topic to deserve its own tutorial.

Committing secrets or cache files. Files like __pycache__/, .pyc files, or a local .env holding real credentials will show up as untracked and, if you’re not paying attention, get swept into git add .:

git status --short
?? .env
?? __pycache__/

The .gitignore from Step 6 stops this before it happens; add patterns for anything generated or sensitive as soon as you notice Git tracking it. If a real secret does get committed and pushed, treat it as compromised: rotate it at the source (regenerate the API key, change the password) rather than relying on deleting the file, since the old value stays recoverable from history.

Detached HEAD state. If you ever run git checkout <some-commit-hash> directly instead of a branch name, Git will warn you that you’re in a “detached HEAD” state. You can look around and even make commits, but they won’t belong to any branch and are easy to lose. Run git checkout main (or any branch name) to get back to normal, and if you made commits you want to keep, git branch new-branch-name while still detached will save them.

Verify everything works end-to-end

  1. On GitHub, confirm the repository shows your latest commit message on main and that the file list matches what’s on your machine.
  2. Locally, run git status; it should report nothing to commit, working tree clean and Your branch is up to date with 'origin/main'.
  3. Run git log --oneline --graph --all. You should see your initial commit, the merge commit GitHub created for the pull request, and the feature branch’s commit, all connected.
  4. From a different directory, try git clone [email protected]:your-username/server-toolkit.git test-clone. A clean clone that pulls down every file over SSH with no password prompt confirms your key setup works end to end, not just from the directory you originally configured it in.

Next steps

From here, the workflow you just learned, branch, commit, push, pull request, merge, is the same one used on projects with thousands of contributors; the only thing that changes at scale is how much review a pull request gets before merging. A few natural next steps: read up on resolving a merge conflict, which happens when two branches change the same lines of the same file (this tutorial’s divergent-history example above never touched the same lines, so Git could merge automatically); look into branch protection rules in your repository’s settings, which can require pull request review or passing checks before anyone, including you, can push straight to main; and once you’re comfortable with the basics, learn git rebase as an alternative to merging for keeping a branch’s history linear.

Tags:

Developer ToolingGitGitHubSSHVersion Control

Share

European Union flags flying outside the glass facade of the European Commission's Berlaymont building in Brussels
Previous Post

The EU AI Act’s Article 50 Deadline Turns AI Disclosure Into an Accountability Test

A person writes real analysis mathematical proofs on a whiteboard, illustrating formal mathematical proof work
Next Post

OpenAI Reveals Its Next Model, Astra, in a Blog Post About Math Proofs

No Comment! Be the first one.

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Latest
08 Oct
How to Add Backpressure and Load Shedding to a Python Service Before Overload Takes It Down
08 Oct
GitHub’s Git Rebuild Turns Repository Durability and Read Scale Into Two Separate Problems
Trending
October 8, 2026
How to Add Backpressure and Load Shedding to a Python Service Before Overload Takes It Down
October 8, 2026
GitHub’s Git Rebuild Turns Repository Durability and Read Scale Into Two Separate Problems
October 8, 2026
A Compromised Admin Account Put the Shai-Hulud Worm Into AI Sandbox Maker Tensorlake’s npm SDK
October 8, 2026
How to Prevent Broken Object Level Authorization (IDOR) in a FastAPI App
October 8, 2026
Singapore’s AI Guidelines Turn Independent Review Into a Question of Who Sets the Risk Rating
October 8, 2026
Attackers Hijacked the .gh, .sl and .as Country Domains and Minted HTTPS Certificates for Google

Related Posts

A laptop wrapped in a chain and padlock, illustrating least-privilege controls for AI agents.
Learning Hub

How to Secure Tool-Using AI Agents Before They Touch Production

June 8, 2026
Colorful sticky notes arranged on an office wall, symbolizing governance checklists and planning.
Learning Hub

AI Governance for Agentic Apps: A Practical Checklist for Builders

June 8, 2026
A technician connects green fiber optic cables at a data center, representing a private production inference endpoint.
Learning Hub

How to Deploy a Fine-Tuned LLM Behind a Private Production Inference Endpoint

June 8, 2026
Narrow aisle behind black supercomputer racks in a data center
Learning Hub

Kubernetes SELinux Volume Labeling: What Cluster Operators Should Audit Before v1.37

June 8, 2026
SXZ.io SXZ.io
  • [email protected]

Categories

Articles
Learning Hub
News

All Rights Reserved by SXZ.io ©2026