Fork It, Keep It Private, Stay in Sync with Upstream

Table of Contents
Ever found an open source project that does most of what you need, but not quite all of it? I run into this all the time. You want to add your own features, keep them private, and still pull in upstream bug fixes without losing your mind.
The standard fork workflow on GitHub works fine for public contributions. But what if you're building something proprietary on top of someone else's open source work? You need a different approach.
Here's the setup I've landed on after burning through a few different strategies. It's not fancy. But it works.
The Goal
Keep a private fork with your custom commits applied cleanly on top of the latest upstream changes. No merge noise. No manual conflict nightmares.
The Setup
First, clone the original project. Then swap the remotes so your private repo becomes origin and the original becomes upstream.
git clone https://github.com/original-author/some-project
cd some-project
git remote set-url origin https://github.com/you/your-private-repo
git remote add upstream https://github.com/original-author/some-project
git push origin main
Why do it this way? Two reasons. First, origin is git's default remote. When you type git push without thinking, it goes to your private repo. That's safe. Second, keeping the original as upstream makes it obvious where updates come from.
I've seen people skip this and just push directly to their fork's remote. That works too. But I like being explicit about which remote is which. It saves me from pushing something to the wrong place at 11pm.
The Sync Loop
Here's where the real workflow lives. The original project keeps moving. You need to pull those changes in without breaking your custom work.
git fetch upstream
git checkout main
git rebase upstream/main
git push origin main --force-with-lease
Four commands. That's it. Let me walk through why each one matters.
Fetch, Don't Pull
git fetch upstream downloads the latest commits from the original project. It doesn't merge them into your branch. That's deliberate. You want to see what's coming before you apply it.
I used to run git pull upstream main out of habit. Bad idea. Pull does a fetch and an implicit merge. You lose control over how your changes get combined with theirs.
The Rebase
git rebase upstream/main rewinds your commits, applies the upstream changes, then replays your commits on top. Your custom work stays on top. The upstream changes stay below.
Why Rebase Instead of Merge
Merge commits serve a purpose in collaborative team work. But for a solo fork sync, they're noise. Every time you run git merge, you create a commit that says "merged branch x into branch y." Do that a dozen times and your history becomes a mess of empty merge bubbles. Rebase keeps it linear and readable.
Force Push With a Safety Net
Rebasing rewrites history. The commit hashes change. Git will yell at you if you try a normal push. That's where --force-with-lease comes in.
git push origin main --force-with-lease
This is safer than a plain --force. It checks that nobody else has pushed to the remote branch since your last fetch. If they have, git rejects the push. You don't overwrite anyone's work by accident.
Lease, Not Force
A plain --force pushes blindly. --force-with-lease is your safety belt. Use it. Every time.
The Merge Alternative
Some people argue for merge over rebase. They say rebasing rewrites history and that's bad. I get the argument. In a shared branch with multiple collaborators, I'd agree. Merge commits create a record of when things happened.
But for a private fork? You're the only one working on it. A clean linear history is more valuable than a timestamped record of every sync. You want to look back in six months and see your custom commits stacked neatly on top of upstream releases. Not a tangled web of merge bubbles.
If you are working with a team on the fork, use merge. Communicate with your team. Pick one strategy and stick with it. Nothing worse than half the team rebasing and the other half merging.
Handling Conflicts
Rebasing will occasionally produce conflicts. It's not fun, but it's manageable.
git rebase upstream/main
# git says: conflict in some-file.ts
# Fix the file manually
git add some-file.ts
git rebase --continue
The key insight: resolve conflicts in your code, not in theirs. The upstream changes are facts. Your custom modifications need to adapt to whatever the upstream maintainers changed.
If the conflict is too large, you have options:
- Abort and merge instead:
git rebase --abortundoes everything. Trygit merge upstream/mainif you're pressed for time. - Split your commit: A big commit that touches many files conflicts more often. Keep your commits small and focused.
- Reach out: If upstream changed an API you depend on, the maintainers might have migration advice.
Conflict Mindset
The upstream doesn't know about your fork. When they refactor internals, they'll break your custom code. That's normal. Budget time for conflict resolution after big upstream releases.
When Not to Use This Workflow
This approach isn't right for every situation. Here's when I'd pick something else:
- You plan to contribute back: Just fork publicly on GitHub and submit PRs. No need for the private repo dance.
- The upstream changes constantly: Daily releases mean constant rebasing. Consider vendoring the dependency instead.
- Your changes are huge: If you're rewriting half the project, maintaining a fork adds friction. You might be better off building your own thing.
- The license won't let you: This is the big one.
License Compatibility
Before making any fork private, read the upstream license. MIT and Apache 2.0 allow private forks. GPL and AGPL require you to publish modifications if you distribute the software. This isn't optional. Violating a license can get you into legal trouble.
A Quick Reference
Here's the full workflow in one block. Bookmark it.
# Initial setup
git clone https://github.com/original-author/some-project
cd some-project
git remote set-url origin https://github.com/you/your-private-repo
git remote add upstream https://github.com/original-author/some-project
git push origin main
# Each sync cycle
git fetch upstream
git checkout main
git rebase upstream/main
git push origin main --force-with-lease
Final Thoughts
I've been using this workflow for about two years across several projects. It's not perfect. Conflicts still happen. Large rebases can be tedious. But it beats the alternatives.
The alternatives are worse. Either you never sync upstream and your fork rots. Or you merge blindly and your history turns into a knot that nobody can read. Or you give up on privacy and open source everything.
You Own Your Workflow
Pick the approach that matches your situation. For me, a private fork with periodic rebase syncs has been the sweet spot between staying current and keeping control. Your mileage will vary. That's fine.
You Might Also Like
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Python's Underscores: Conventions, Not Access Modifiers
Python uses underscores as naming conventions, not enforced access modifiers—understanding the difference prevents confusing code and helps you write better APIs.
Read more
Why I'm Learning Rust as a Web Developer (And You Should Too)
Rust isn't just for systems programmers. Here's why web developers are picking it up, and how six months with the borrow checker has changed how I think about JavaScript and Python.
Read more
How I Set Up CI/CD with GitHub Actions
A practical walkthrough of setting up GitHub Actions workflows for a typical web project — running tests, caching dependencies, deploying to Vercel, handling secrets, and avoiding the common mistakes that make CI slow or unreliable.
Read more