
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.
Keep a private fork with your custom commits applied cleanly on top of the latest upstream changes. No merge noise. No manual conflict nightmares.
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.
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.
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.
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.
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.
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.
A plain --force pushes blindly. --force-with-lease is your safety belt. Use it. Every time.
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.
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:
git rebase --abort undoes everything. Try git merge upstream/main if you're pressed for time.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.
This approach isn't right for every situation. Here's when I'd pick something else:
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.
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
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.
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've been diligently using the `git rebase` workflow to keep your private fork in sync with an upstream open-source project. One day, you attempt to `git push origin main --force-with-lease` after a rebase, and the push is rejected. What is the most likely reason for this rejection in a real-world scenario?
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

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
Scaling Kubernetes pods based on business metrics: Prometheus Adapter setup, custom PromQL queries, and HPA stabilization windows.
Read more
Deep dive into eBPF observability in Kubernetes: eliminating Envoy sidecars, kernel probes, BPF ring buffers, and zero-code telemetry instrumentation.
Read more