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 where | Created by | Purpose | |
|---|---|---|---|
| Branch | Inside one repository | Git | Diverge work within a repository you can push to |
| Clone | Your machine | Git | Get a local working copy |
| Fork | GitHub’s servers | GitHub | Get 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.
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.
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.
What GitHub records
Section titled “What GitHub records”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.
Setting up a fork for real work
Section titled “Setting up a fork for real work”Fork and clone in one command:
gh repo fork owner/project --cloneWhat 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:
git remote -vOutput:
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.
Keeping a fork current
Section titled “Keeping a fork current”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:
gh repo sync you/project --branch mainOr with Git, which is worth knowing because it shows what is actually happening:
git fetch upstreamgit switch maingit merge --ff-only upstream/maingit push origin mainWhat 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.
Opening the pull request
Section titled “Opening the pull request”With work pushed to a branch on your fork:
gh pr create --repo owner/project --base main --head you:my-feature \ --title "Add retry handling to the client" \ --fillThe --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.
What actually happens to the Git data
Section titled “What actually happens to the Git data”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:
git log --oneline -1 upstream/maingit log --oneline -1 origin/mainOutput shortly after forking:
a3f8c21 Fix retry backoff calculationa3f8c21 Fix retry backoff calculationIdentical, 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.
Forks in open-source contribution
Section titled “Forks in open-source contribution”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.
Fork networks and their surprises
Section titled “Fork networks and their surprises”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.
When a fork is the wrong tool
Section titled “When a fork is the wrong tool”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.
Common mistakes
Section titled “Common mistakes”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:
gh repo list --fork --limit 100 --json nameWithOwner,updatedAt,parentExercise
Section titled “Exercise”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.
- Fork and clone in one step with
gh repo fork owner/project --clone. - Verify
originandupstreamwithgit remote -v. - Create a branch, make a small commit, and push it to
origin. - Open a pull request against the upstream with
gh pr create --repo owner/project. - Sync your fork with
gh repo sync, then confirm your branch is still based on current upstream history. - Delete the fork with
gh repo deleteonce 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.
What you learned
Section titled “What you learned”- A fork is a server-side copy plus a recorded relationship; the relationship is what enables cross-repository pull requests.
originis your fork andupstreamis 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
mainpristine 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.
Related lessons
Section titled “Related lessons”Check your understanding
4 questions — each one asks you to predict what Git or GitHub will do, not to recall a flag.