Figure 1. Refs belong to each repository. Objects belong to the network.
Fork
A fork is a copy of a repository under a new owner that stays linked to the original; on GitHub, forks share Git objects across one fork network.
Also known as GitHub fork, repository fork, forked repository, fork network
A fork, also called a repository fork or GitHub fork, is a copy of a repository created under a new owner that stays linked to the original, called the upstream. On GitHub, every fork joins the upstream's fork network and shares its Git object storage, so code pushed to any fork can be fetched through the upstream repository's URL.
That shared storage is the most important fact about forks for security. A commit SHA under github.com/owner/project proves which bytes you got, not that the owner wrote them or ever accepted them. The TanStack compromise in May 2026 depended on exactly this: its malicious npm releases installed github:tanstack/router#79ac49e…, a commit TanStack's postmortem calls an "orphan payload commit (in fork network)." Our position: a fork is a different publisher that shares a name and a database with the original. Trust a commit only when a ref the upstream controls reaches it, and never infer identity from resemblance.
What is a fork in GitHub?
#A fork in GitHub is a separate repository, owned by another account, that starts as a copy of an upstream repository and stays connected to it. GitHub's documentation draws the line against branches plainly: "A branch is part of one repository. A fork is a separate repository with its own settings and collaboration space." The fork gets its own issues, pull requests, Actions workflows, and permissions. Forks are how outsiders propose changes to projects they cannot write to: they push to their fork, then open a pull request against the upstream.
The word is older than GitHub. In open source, forking a project has long meant a group taking a codebase in a new direction under new maintainers, as Valkey did with Redis in March 2024. On GitHub, fork also names a button and a data model, and the data model is what matters for security. The term also covers the Unix fork() system call, which duplicates a process, and blockchain forks, where a chain's rules split. This entry covers neither.
| Property | Branch | Clone | Fork |
|---|---|---|---|
| Where it lives | Inside one repository | Any machine that ran git clone | On GitHub, under a new owner |
| Who can write to it | Anyone with push access to that repository | Whoever holds the copy | The fork's owner and their collaborators |
| Link to the original | It is the original repository | A remote URL in .git/config, nothing on GitHub | "forked from" label, API parent and source fields, shared fork network |
| Shares Git objects with the original | Yes, same repository | No, objects are copied once | Yes, across the whole fork network |
| Use it when | You have write access | You want a local working copy | You lack write access, or need an independent copy on GitHub |
Verdict: use a branch whenever you have write access, because a branch keeps review and CI inside one trust boundary. Fork only to contribute without write access, or to take over maintenance. Never consume someone else's fork through a Git URL as if it were the original project.
How do you fork a repo on GitHub?
#You fork a repo on GitHub by opening the repository, clicking Fork, choosing an owner, and clicking Create fork. GitHub's walkthrough uses its practice repository, octocat/Spoon-Knife. The form lets you rename the fork, and its Copy the DEFAULT branch only option decides whether other branches come along.
From a terminal, the GitHub CLI does the same work and wires up remotes. Per the gh manual, the new fork becomes origin and an existing origin is renamed upstream:
# Fork and clone in one step
gh repo fork octocat/Spoon-Knife --clone
# Or, after forking in the web UI, add the upstream remote by hand
git clone https://github.com/YOUR-USERNAME/Spoon-Knife
cd Spoon-Knife
git remote add upstream https://github.com/octocat/Spoon-Knife.git
# Pull in upstream changes later
git fetch upstream
git merge upstream/main
git push origin mainThe web UI's Sync fork button and gh repo sync do the merge on GitHub. Neither runs on its own. A fork that nobody syncs keeps every bug, and every vulnerability, from the day it was created.
A GitHub fork network shares code by keeping one pool of Git objects for the upstream and all of its forks, while each repository keeps its own refs. GitHub documents this: "Git data from any repository in a network may be accessed from any repository in the same network," including the upstream, and that holds "even after a fork is deleted."
From first principles: Git names every commit, tree, and file by the hash of its content, and a ref such as refs/heads/main is just a name holding one of those hashes. A Git server decides which objects to hand out when a client asks for a SHA. Deduplicating storage across forks is a sensible choice for a host with millions of near-identical copies. The trust assumption breaks one layer up: the URL a client fetches from names a repository, but the objects it can receive belong to the whole network.
Figure 1 shows three repositories in one network. The upstream, owner/project, has refs/heads/main pointing at commit A and refs/pull/7/head pointing at commit C, a fork's pull request. A fork, you/project, has main pointing at commit B, which exists only in that fork. A renamed fork, attacker/configuration, has main pointing at commit X, an orphan payload. All four commits sit in one shared object store. Running git fetch against owner/project for commit X succeeds, even though no ref in owner/project points to X.
Pull requests are the visible half of this. When someone opens a pull request from a fork, GitHub writes a ref named refs/pull/<number>/head into the upstream repository, pointing at the fork's commit. We counted them on GitHub's practice repository on November 3, 2026:
$ git ls-remote https://github.com/octocat/Spoon-Knife | wc -l
64113
$ git ls-remote https://github.com/octocat/Spoon-Knife 'refs/heads/*' | wc -l
3
$ git ls-remote https://github.com/octocat/Spoon-Knife 'refs/pull/*/head' | wc -l
37779The repository has 3 branches and 37,779 pull request heads. Nearly every head is code written outside the project, served under octocat's name.
The invisible half is commits nobody ever offered as a pull request. We took the tip of one fork's main branch, confirmed it is not the tip of any of the upstream's 64,113 refs, and fetched it through the upstream URL. As a control, we asked for a commit from actions/checkout, which belongs to a different network:
$ git ls-remote https://github.com/octocat/Spoon-Knife | grep -c 2a0cbc23b72e55030a399279261e2b1f75cd8f94
0
$ git init -q demo && cd demo
$ git fetch --depth=1 https://github.com/octocat/Spoon-Knife 2a0cbc23b72e55030a399279261e2b1f75cd8f94
From https://github.com/octocat/Spoon-Knife
* branch 2a0cbc23b72e55030a399279261e2b1f75cd8f94 -> FETCH_HEAD
$ git log -1 --format=%s FETCH_HEAD
Update styles.css
$ git fetch --depth=1 https://github.com/octocat/Spoon-Knife 11d5960a326750d5838078e36cf38b85af677262
fatal: remote error: upload-pack: not our ref 11d5960a326750d5838078e36cf38b85af677262The boundary is the network, not the repository. You can reproduce this with any fork you own: push a commit to it, then fetch that SHA through the upstream URL.
The network also outlives its members. GitHub's rules for deletion and visibility changes are:
- Deleting a public repository: "An active public fork becomes the new upstream repository for the network."
- Deleting a private repository: its private forks are deleted too.
- Making a public repository private: its public forks stay public, in a separate network.
- Making a private repository public: its private forks stay private and split into separate private networks.
Truffle Security showed what this means for secrets in July 2024. Its research on Cross Fork Object Reference (CFOR) found that commits pushed to deleted forks stay reachable through the upstream. Truffle recovered 40 valid API keys from three commonly forked repositories this way. Deleting a fork does not delete its commits.
Impostor commits: fork code under the upstream's name
#An impostor commit is a commit pushed to a fork and referenced through the upstream repository's name. A reference that reads owner/project@<sha> can therefore run code the owner never wrote. Anything that resolves Git references by SHA inherits the problem, and two widely used resolvers do: npm Git dependencies and GitHub Actions uses: lines.
The TanStack compromise used the fork network twice. We reconstructed the fork's part from TanStack's postmortem, all times UTC:
- May 10, 2026, 17:16. The attacker forks TanStack/router as zblgg/configuration, a fork "deliberately renamed to evade fork-list searches."
- May 10, 23:29. A malicious commit, 65bf499d, is authored on the fork under a fabricated identity.
- May 11, about 10:49. Pull request #7378, "WIP: simplify history build," opens against TanStack/router. A privileged
pull_request_targetworkflow runs it and poisons the shared Actions cache. Attacker code later runs inside the release workflow and extracts an OIDC token. - Later on May 11. 84 malicious versions across 42 @tanstack packages reach npm through the project's trusted-publisher binding.
Each malicious package.json carried one new entry:
"optionalDependencies": {
"@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}Commit 79ac49e was never part of TanStack's history. On install, npm cloned tanstack/router, asked for that SHA, and GitHub served it from the network. A Git dependency gets built before it is installed, and npm's documentation says "the prepare script will be run" for one. The payload's prepare script did the rest. The postmortem's open question is the hard part: every fork that still holds the commit keeps it reachable as github:tanstack/router#79ac49ee….
Socket's GitHub dependency alert is a high-severity alert for any package with a dependency that resolves to a GitHub URL. The TanStack entry has exactly that shape. The alert fires on the shape of the reference, whatever commit it names. That is the right granularity, because the shape is the risk: a registry package is about to fetch code the registry never saw.
GitHub Actions has the same hole. A step written uses: actions/checkout@<sha> resolves any SHA in actions/checkout's network, including one that exists only in a fork. GitHub's workflow lockfiles, in public preview since October 14, 2026, close this for the actions they cover. As our hardening guide explains, a locked commit must be reachable from its recorded branch or tag, so a fork-only commit is rejected. Workflows that are not onboarded need an explicit reachability check, such as zizmor's impostor-commit audit. Our entry on GitHub Actions security covers the fork-triggered workflows that let #7378 in.
How do attackers use malicious forks and lookalike repos?
#Attackers use malicious forks and lookalike repos to borrow a project's name, history, and audience. The Fork button is only one way to make a copy, and often not the attacker's first choice.
Copying a repository by cloning it and pushing it to a new account carries over almost everything a visitor checks:
- The name. Repository names are unique only per owner, so anyone can create attacker/project.
- The full history, with the original authors' names and emails. Git author fields are unauthenticated text.
- The README, topics, and release notes.
A clone-and-push copy is not a fork in GitHub's data model. The REST API returns parent and source objects, which "are present when the repository is a fork," and the page shows a "forked from" line. A copy made by pushing has neither, so it presents as an original. What no copy can take is the owner account, its numeric ID, the upstream's refs, and the package name on the registry.
Three patterns recur.
Repo confusion. In a campaign Apiiro reported on February 28, 2024, attackers cloned real repositories, added a malware loader, re-uploaded them under the same names, then forked each copy thousands of times for reach. Apiiro detected more than 100,000 infected repositories. GitHub's automated cleanup removed most of them, but about 1% survived, which still meant thousands. In Apiiro's words, these attacks "simply rely on humans to mistakenly pick the malicious version over the real one."
Forks as packages. In Go, a module path is usually a repository URL, so a lookalike repository is a lookalike package. That makes Go modules the cleanest case of a fork turning into typosquatting. The shopsprint/decimal typosquat of shopspring/decimal mirrored upstream releases for six years, which in our analysis gave it "the appearance of a maintained fork." Version v1.3.3 added a DNS backdoor on August 19, 2023, and with it net and os/exec imports that no decimal library needs. That is why we analyze what a package contains rather than what its repository claims to be. The boltdb-go/bolt typosquat impersonated boltdb/bolt, an archived project whose users are pushed toward forks. After the Go module proxy cached the backdoored version, the attacker rewrote the tag on GitHub, so the repository looked clean while go get kept serving malware.
Forks as a way in. A pull request from a fork is the standard way for a stranger to get code into a project's CI. GitHub's pull_request trigger does not pass secrets to a run triggered from a fork, and it gives the token read-only permissions. The pull_request_target trigger runs with the base repository's privileges, which is how #7378 reached TanStack's cache. In June 2026, actions/checkout v7 began refusing fork checkouts in those workflows. That guard covers only one action.
Depending on a fork means trusting a new maintainer
#Depending on a fork hands your build to the fork's owner, who now decides what ships to you. Plenty of forks deserve that trust. Valkey, started by former Redis contributors under the Linux Foundation, is one. Abandoned projects produce forks too: when an upstream is archived, as boltdb/bolt was, users drift to whichever fork looks active. Our entry on end-of-life dependencies covers that choice. The decision is the same either way: you are adding a dependency with a new maintainer, not updating an old one.
The way you consume a fork decides how much you inherit:
- An npm Git dependency such as
github:you/project#<sha>records no integrity hash and resolves any SHA in the network. Through npm 11 it also runs the fork'spreparescript on install. npm v12, released July 8, 2026, refuses Git dependencies unless you pass--allow-git, and blocks theirpreparescripts unless you allow them. - A Go
replacedirective, such asreplace golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5from the Go modules reference, swaps the source for one module. The fork'sgo.modmust still declare the original module path. - A fork published as its own package, such as a scoped npm package or a module path you control, gets an immutable version, a checksum, and the same scanning as everything else.
We recommend the last option. It costs a publish step for every change. It still does not solve patch lag: upstream security fixes reach your fork only when someone merges them, and nothing in GitHub does that for you.
A repository's name is not its identity
#Every signal a visitor uses to recognize a repository can be copied or borrowed. The name is free to register under any owner. The history and authors come along with a clone. A commit SHA under the right URL can come from anyone in the network, and an owner name can change hands, which is how repojacking works. What identifies a GitHub repository is the account that owns it, its numeric repository ID, and the refs that account controls.
We think GitHub should stop resolving fork-only commits under the upstream's name, at least for git fetch by SHA and for Actions uses: references. Until it does, every tool that consumes owner/project#sha has to check that the commit is reachable from a ref the owner controls. npm does not, and neither does a workflow without a lockfile. A fork network is one trust domain with many owners. Today the URL tells you only whose name will appear in the postmortem.
