GitHub Branch Protection on Private Repos — Without Paying for Team Plan
GitHub Rulesets on private repos require the Team plan ($4/user/month). If you have a live production app and want to stop direct pushes tomainwithout paying, here is a GitHub Actions workaround that gets you 80% of the way there for free.
The Problem
You try to set up branch protection on a private repo and GitHub shows you this:
Your rulesets won't be enforced on this private repository
until you upgrade this organization account to GitHub Team.The old branch protection rules UI also prompts an upgrade for private repos. For a small team or solo developer, $4/user/month is an unnecessary cost when you just want one simple rule: no direct pushes to main.
The Workaround
GitHub Actions runs on every push — including direct pushes tomain. Using the GitHub API, the workflow can check whether the incoming commit came from a merged pull request or was pushed directly. If it was a direct push, the workflow fails loudly.
This does not block the push (nothing free can do that on private repos), but it immediately fails CI, making the violation visible to the whole team and blocking any downstream deploy workflows from running.
The Workflow
Create .github/workflows/protect-main.yml in your repo:
name: Protect Main Branch
on:
push:
branches: [main]
jobs:
check-pr-merge:
runs-on: ubuntu-latest
steps:
- name: Check if push came from a merged PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
COMMIT_SHA="${{ github.sha }}"
PUSHER="${{ github.actor }}"
# Check if this commit is associated with a merged PR
PR=$(gh api repos/${{ github.repository }}/commits/$COMMIT_SHA/pulls \
--jq '.[0].merged_at' 2>/dev/null || echo "null")
if [ "$PR" = "null" ] || [ -z "$PR" ]; then
echo "❌ Direct push to main detected by $PUSHER"
echo "All changes must go through a pull request."
exit 1
fi
echo "✅ Commit came from a merged PR. Allowed."secrets.GITHUB_TOKEN is automatically available in every Actions run — no setup needed.
Block the Deploy Too
The real enforcement comes from making your deploy workflow depend on this check. Add needs: check-pr-merge to your deploy job:
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
check-pr-merge:
uses: ./.github/workflows/protect-main.yml # reuse the check
deploy:
runs-on: ubuntu-latest
needs: check-pr-merge # deploy only runs if check passes
steps:
- name: Deploy
run: echo "deploying..."Now a direct push to main fails the check, and because deploy needs the check, the deploy never runs. Production is safe.
What This Gives You
✅ Direct push to main → CI fails immediately
✅ Deploy workflow blocked (needs check-pr-merge)
✅ Violation is visible in GitHub Actions tab
✅ Works on free plan, private repos
✅ No setup beyond adding the YAML file
❌ Cannot physically block the push from landing (needs Team plan)
❌ The commit does appear in git history for a few secondsThe Correct Workflow Going Forward
feature branch
→ push to dev
→ PR from dev to main
→ review & approve
→ merge
→ protect-main check passes (came from PR)
→ deploy runs
→ production updatedAny push that bypasses this flow fails CI immediately and never reaches production.
Is It Worth Paying for GitHub Team?
If your team grows beyond 2-3 people, yes — GitHub Team adds:
- True push blocking (the commit never lands)
- Required status checks before merge
- CODEOWNERS — specific reviewers required per file
- Required number of approvals
For a solo developer or a small startup with a live production app, this workaround is a solid interim solution that costs nothing.