Skip to content

GitHub Forks Explained: Fork vs Clone vs Branch

Lesson 4 of 10Beginner8 min readGitHub Engineering · GitHub FundamentalsVerified: gh 2.98.0 on Ubuntu 24.04, August 2026

A fork is a copy of a repository under a different owner, which GitHub records as derived from the original.

The copy is the boring half. The recorded relationship is the half that matters, because it is what makes a pull request from your copy to somebody else’s repository possible at all. Without it, two repositories with identical history are simply two unrelated repositories.

Fork, clone, branch: three different things

Section titled “Fork, clone, branch: three different things”

These get used interchangeably in conversation and mean entirely different things.

Lives whereCreated byPurpose
BranchInside one repositoryGitDiverge work within a repository you can push to
CloneYour machineGitGet a local working copy
ForkGitHub’s serversGitHubGet a copy you own, linked to the original

The decision is really about write access:

  • You can push to the repository → use a branch.
  • You cannot push to it, and want to propose a change → use a fork.
  • You just want the code locally → clone, and stop there.

That last case is worth emphasising. If you want to read, build or run somebody’s project, you do not need a fork. git clone gives you the complete history. Forking to “get a copy” leaves a permanent public artefact on your profile that you now have to maintain or delete.

The contribution path through a fork

A vertical chain: the upstream repository is forked to your GitHub account; the fork is cloned to your local machine; a branch is created locally; commits are pushed back to the fork; and a pull request is opened from the fork's branch to the upstream repository.

Upstream repositoryfork (on GitHub)Your forkgit cloneLocal clonegit switch -cFeature branchgit push (to fork)Pull request → upstream

Note what moves where. You push to your fork, never to the upstream — you have no write access there. The pull request is the mechanism by which the upstream maintainer can pull your commits into their repository, which is where the name comes from.

When you fork, GitHub creates a new repository owned by you, containing the upstream’s history, and stores the parent relationship. Two consequences follow.

Cross-repository pull requests become possible. The pull request interface lets you select a base branch in one repository and a head branch in another, because GitHub knows the two are related.

The fork joins a network. All forks of a repository, and forks of those forks, share an object store. This is an efficiency measure — GitHub does not store a hundred copies of the same history — but it has visible effects, discussed below.

Fork and clone in one command:

Terminal window
gh repo fork owner/project --clone

What it doesForks the repository to your account and clones your fork locally, configuring the original as a remote named `upstream`.

Why we run itDoing this by hand means forking in a browser, copying a URL, cloning, then adding a second remote. The CLI does all four steps and gets the remote naming convention right.

Expected resultConfirmation that the fork was created, then clone output, then a note that upstream was added.

Confirm the two remotes:

Terminal window
git remote -v

Output:

origin git@github.com:you/project.git (fetch)
origin git@github.com:you/project.git (push)
upstream https://github.com/owner/project.git (fetch)
upstream https://github.com/owner/project.git (push)

The convention is worth internalising: origin is yours, upstream is theirs. You push to origin and fetch from upstream. Getting these backwards produces the most common fork confusion — trying to push to a repository you cannot write to and receiving a permission error that looks like an authentication problem.

A fork is a snapshot. It does not track the upstream automatically; the moment the original moves, yours is behind.

This matters because a pull request built on stale history creates conflicts and asks reviewers to evaluate a change against a codebase that no longer exists.

The direct way:

Terminal window
gh repo sync you/project --branch main

Or with Git, which is worth knowing because it shows what is actually happening:

Terminal window
git fetch upstream
git switch main
git merge --ff-only upstream/main
git push origin main

What it doesFetches the upstream's latest commits, moves your local main to match them exactly, and pushes that to your fork.

Why we run it`--ff-only` refuses to create a merge commit. If your main has diverged, you want to know rather than silently merging — a fork's main should normally be a clean copy of upstream's.

Expected resultFetch output, a fast-forward summary, then push output. If it refuses, your main has commits upstream does not.

With work pushed to a branch on your fork:

Terminal window
gh pr create --repo owner/project --base main --head you:my-feature \
--title "Add retry handling to the client" \
--fill

The --head you:my-feature form is specific to cross-repository pull requests: it names the owner of the fork as well as the branch. Without the owner prefix, GitHub looks for the branch in the upstream repository and does not find it.

Maintainers can usually push to your pull request branch, provided you leave the allow edits from maintainers option enabled. This lets them fix a small issue rather than requesting a change and waiting — usually worth accepting, though be aware it grants write access to that branch of your fork.

The full pull request workflow, including reviews and merging, is the Pull Requests cluster. This lesson only covers the fork-specific part: that the head branch lives somewhere you own and the base branch does not.

It is worth being concrete, because “copy” is doing a lot of work in the usual explanation.

A fork starts with exactly the same commit objects as the upstream. Same SHAs, same trees, same blobs. Nothing is rewritten, which is why a commit you can see in the upstream is findable by the same SHA in a fork.

From that shared starting point the two diverge independently:

Terminal window
git log --oneline -1 upstream/main
git log --oneline -1 origin/main

Output shortly after forking:

a3f8c21 Fix retry backoff calculation
a3f8c21 Fix retry backoff calculation

Identical, because nothing has happened yet. Once the upstream merges anything, those two lines differ, and the difference is exactly what gh repo sync closes.

This is why syncing is a fast-forward rather than a merge when you have kept your main clean: your main is an ancestor of the upstream’s main, so Git only has to move the pointer forward. The moment you commit to your own main, the two histories have diverged and Git can no longer simply advance the ref — it has to reconcile two lines of development, which is a merge, with everything that entails.

The fork-based model exists because of a permission problem: a project with thousands of potential contributors cannot grant write access to all of them, and does not want to evaluate each person before seeing their code.

Forking inverts the trust model. Anyone can copy a public repository and push whatever they like to their own copy, because it is theirs. The pull request is then a request — the maintainer reviews the change and decides. At no point does the contributor gain write access to the upstream.

That has some practical implications when contributing:

Read the contribution guidelines first. Most established projects have CONTRIBUTING.md. It usually specifies the branch to target, commit message conventions, whether an Issue must exist first, and how tests are run. Ignoring it is the most common reason a technically fine pull request stalls.

One change per pull request. Reviewers of open-source contributions have no context on you and limited time. A focused change is reviewable; a fifteen-file change touching four concerns is not. PR Best Practices covers this in depth.

Expect CI to behave differently. Workflows triggered by pull requests from forks run with restricted permissions and no access to repository secrets. This is a deliberate security boundary: otherwise anyone could open a pull request that exfiltrated the project’s credentials. If a check appears to fail for an unexplained permissions reason, this is usually why.

Your branch may be squashed on merge. Many projects squash-merge contributions, which replaces your carefully separated commits with one. That is a project convention rather than a judgement on your history — see Squash Merging.

Shared object storage across a network produces behaviour that looks like a bug and is not.

Commits pushed to a fork of a public repository can remain reachable through the network, even after the fork is deleted, and even if they were never merged. This is why pull requests from deleted forks still render their diffs. It also means a secret pushed to a fork of a public repository should be treated as published — see Public vs Private Repositories.

Private forks inherit private visibility and can only be made by users who already have access. Organisations may disable forking of private repositories entirely.

Deleting the upstream does not delete forks. GitHub promotes one fork to become the new network root. Popular abandoned projects continue to exist through their forks.

Forks solve one problem: contributing without write access. Used for anything else they add cost.

To take a copy for yourself. Clone it. A fork is public, appears on your profile, and drifts.

To maintain a permanent divergence. If you intend to modify a project substantially and never merge back, a fork’s relationship to upstream buys you nothing. Consider a genuine independent repository, which is honest about the situation.

To keep a vendored dependency. Use your package manager, a submodule, or a vendoring tool.

Inside a team where everyone has write access. Fork-based workflows exist to handle untrusted contributors. Within a team, branches in one repository are simpler: one place to look, no syncing, no cross-repository pull requests. Some teams use forks for isolation anyway; that is a deliberate trade, not a default.

Pushing to upstream. You cannot; the error is a permissions failure that reads like an auth problem.

Working on your fork’s main. Turns every sync into a merge.

Opening a pull request from a stale fork. Sync first, then branch.

Forking to read code. Clone instead.

Forgetting the owner prefix in --head. GitHub looks for the branch in the wrong repository.

Leaving forks behind indefinitely. Once merged, the fork has done its job. Audit occasionally:

Terminal window
gh repo list --fork --limit 100 --json nameWithOwner,updatedAt,parent

Contribute to a repository you do not own, using a repository built for the purpose — many organisations publish practice repositories, or use a second account of your own.

  1. Fork and clone in one step with gh repo fork owner/project --clone.
  2. Verify origin and upstream with git remote -v.
  3. Create a branch, make a small commit, and push it to origin.
  4. Open a pull request against the upstream with gh pr create --repo owner/project.
  5. Sync your fork with gh repo sync, then confirm your branch is still based on current upstream history.
  6. Delete the fork with gh repo delete once you are finished.

Step 5 is the one worth doing carefully — noticing what changes after a sync is what makes the relationship between the two repositories concrete.

  • A fork is a server-side copy plus a recorded relationship; the relationship is what enables cross-repository pull requests.
  • origin is your fork and upstream is the original — reversing them causes the most common fork error.
  • Forks do not sync themselves, and stale forks produce avoidable conflicts.
  • Keep your fork’s main pristine and work on branches, so syncing stays a fast-forward.
  • Networks share objects, so commits pushed to a public fork can outlive the fork.
  • If you have write access, use a branch. If you just want the code, clone.

Check your understanding

4 questions — each one asks you to predict what Git or GitHub will do, not to recall a flag.

In the standard fork workflow, what do `origin` and `upstream` refer to?
Show answer

`origin` is your fork; `upstream` is the original project — Reversing them is the most common fork error — and the resulting push failure reads like an authentication problem when it is a permissions one.

Why keep your fork's `main` branch untouched and work on feature branches?
Show answer

So syncing with upstream stays a fast-forward instead of becoming a merge every time — If `main` has your commits, every sync with upstream is a merge. A pristine `main` fast-forwards cleanly and branches rebase onto it.

You have write access to a repository. Should you fork it to contribute?
Show answer

No — use a branch; forking is for when you lack write access — Forks exist to let people without push access contribute. With write access, a branch is simpler and keeps everything in one place.

You force-push over a commit in your public fork to remove a secret. Is it gone?
Show answer

Not necessarily — networks share objects, so commits pushed to a public fork can outlive the fork — Objects are shared across the fork network. Treat anything pushed publicly as published, and rotate the secret.

Free Git Engineer Cheat Sheet BundleSix printable references plus downloadable toolkit files — commands, recovery, aliases, a CODEOWNERS starter. No email required.