By Kedar Salunkhe | Updated March 2026
You are mid-deployment. The pipeline fires, and then — nothing. Your terminal drops this:
! [remote rejected] main -> main (protected branch hook declined)
error: failed to push some refs to 'https://github.com/org/repo.git'
Sound familiar? A git push rejected error in a production CI/CD pipeline is one of those problems that looks simple on the surface but can have five different root causes — branch protection rules, diverged commit history, pre-receive hooks, SSH key mismatches, or a misconfigured pipeline service account.
In this post, I will walk through a real-world CI/CD case study from my work managing multi-cluster Kubernetes environments on Azure AKS and VMware vSphere RKE2, where a push rejection blocked a critical hotfix deployment. We will cover every common cause and the exact fix for each.
Table of Contents
1. What Does ‘Git Push Rejected’ Actually Mean?
When you run git push, Git contacts the remote server and attempts to update the ref (branch or tag) you are targeting. The remote runs a series of checks — built-in and user-defined — and if any check fails, it sends back a rejection response.
The rejection always surfaces in one of two forms:
- A fatal error with an exit code, which breaks your CI pipeline job
- A [rejected] flag in the refs status output, which may or may not fail the pipeline depending on how it is configured
The error message itself is your first diagnostic signal. Here are the most common patterns:
[rejected] (non-fast-forward) --> Your local branch is behind the remote; histories have diverged
[remote rejected] (protected branch hook declined) --> Branch protection rule blocked the push
[remote rejected] (pre-receive hook declined) --> A server-side hook script returned non-zero
Permission denied (publickey) --> SSH key not recognized or wrong key loaded
The requested URL returned error: 403 --> Token expired, wrong scope, or missing write permission
[rejected] (would clobber existing tag) --> Trying to overwrite an existing tag without --force
2. The CI/CD Case Study: Blocked Hotfix in Production
The Scenario
Our team was running a GitHub Actions pipeline for deploying a microservices update to an AKS production cluster. The pipeline had been stable for weeks. Then at 2:47 AM, an on-call alert fired — a misconfigured environment variable was crashing the payments service. The fix was one-line, committed, and pushed immediately.
The GitHub Actions job failed at the git push step with this output:
Run git push origin main
remote: error: GH006: Protected branch update not allowed.
remote: error: At least 1 approving review is required by reviewers with write access.
To https://github.com/prgxlabs/payments-service.git
! [remote rejected] main -> main (protected branch hook declined)
error: failed to push some refs to ‘https://github.com/prgxlabs/payments-service.git’
Error: Process completed with exit code 1.
The Fallout
The CI/CD pipeline was configured to push a release tag back to the repository after a successful deployment — a common pattern in GitOps workflows. Because the service account token used by the pipeline lacked the bypass branch protection permission, the entire deployment was rolled back by the pipeline’s failure handler. The working fix never reached production.
This is the danger of a git push rejected error in CI/CD: it is not just a Git problem. It can trigger rollbacks, page engineers, and cost you 45 minutes of incident resolution time at 3 AM.
Note: The actual application fix was deployed successfully by Kubernetes. The rejection happened on the post-deployment Git tag push — but the pipeline treated the whole job as failed.
3. Cause #1 — Branch Protection Rules
What Is Happening
Branch protection rules on GitHub, GitLab, or Bitbucket prevent direct pushes to protected branches (usually main, master, release/*). These rules commonly require:
- At least N approving pull request reviews
- Status checks to pass before merging
- No force pushes allowed
- Signed commits only
In CI/CD pipelines, a service account or bot token that pushes directly to a protected branch — for example, to update a VERSION file or push a release tag — will hit this wall.
The Fix
Option A: Grant bypass permission to the CI service account. In GitHub: Settings > Branches > Edit rule > Allow specified actors to bypass required pull requests. Add your GitHub Actions bot or deploy key. This is the surgical fix — only the service account gets bypass, not all contributors.
Option B: Push to a non-protected branch, then open a PR via API. Instead of pushing directly to main, push to a ci/release-* branch and use the GitHub REST API to auto-merge if all checks pass:
# In your GitHub Actions workflow
– name: Push release branch
run: |
git checkout -b ci/release-${{ github.sha }}
git push origin ci/release-${{ github.sha }}
– name: Auto-merge via API
run: |
gh pr create –base main –head ci/release-${{ github.sha }} \
–title ‘Release ${{ github.sha }}’ –body ‘Automated release’
gh pr merge –auto –squash
Option C: Use environment-specific tokens with elevated permissions. Create a dedicated GitHub PAT or a GitHub App with the repo and workflows scope, and store it as a protected environment secret. Use this token only in the production deployment job.
Pro tip: GitHub Apps are preferred over PATs for CI/CD because their tokens are short-lived (1 hour) and scoped per installation. PATs are long-lived and risky if leaked.
4. Cause #2 — Non-Fast-Forward / Diverged History
What Is Happening
This is the most common git push rejected error outside of branch protection. It happens when the remote branch has commits that your local branch does not have, meaning the histories have diverged. Git refuses to push because doing so would discard remote commits.
! [rejected] main -> main (non-fast-forward)
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes before pushing again.
In CI/CD, this often happens when:
- Two parallel pipeline jobs both try to push to the same branch at the same time
- A developer pushed a commit manually while the pipeline was running
- The pipeline checks out a specific SHA that is no longer the branch HEAD
The Fix
Safe rebase approach (preserves remote commits):
git fetch origin main
git rebase origin/main
# Resolve any conflicts, then:
git push origin main
Force push with lease (CI/CD safe):
# –force-with-lease only force-pushes if remote has not changed
# since you last fetched. Safer than –force.
git push –force-with-lease origin main
Warning: Never use git push –force in CI/CD without –force-with-lease. A plain –force will silently overwrite commits pushed by others between your fetch and your push.
Prevent races with pipeline concurrency controls:
# GitHub Actions: prevent concurrent runs on same branch
concurrency:
group: production-deploy-${{ github.ref }}
cancel-in-progress: false # queue, do not cancel
5. Cause #3 — Force Push Blocked
What Is Happening
Even when you intentionally want to force-push — for example, to rewrite CI automation commit history or reset a release branch — branch protection rules may block it entirely:
! [remote rejected] main -> main (protected branch, force push is not allowed)
The Fix
- Temporarily disable the ‘Block force pushes’ rule in your repository settings, force-push, then re-enable. Always do this with a second pair of eyes.
- Use the delete-then-recreate pattern for tags: git push –delete origin v1.2.3 followed by git push origin v1.2.3. This avoids the force-push restriction while still updating the tag.
- For branch resets: create a new branch from the correct SHA, open a PR to replace the protected branch, and merge with admin override.
Pro tip: For release tag pushes in CI, always check if the tag already exists before pushing: git ls-remote –tags origin | grep v$VERSION. This prevents the ‘would clobber existing tag’ error entirely.
6. Cause #4 — Pre-Receive Hook Rejections
What Is Happening
Pre-receive hooks are server-side scripts that run before Git accepts any push. They enforce commit message standards, check for secrets, validate branch naming conventions, or run security scans. Unlike branch protection rules, pre-receive hooks are custom scripts that can fail for any reason.
remote: ERROR: Commit message must reference a Jira ticket (e.g., PROJ-1234)
remote: Aborting push.
! [remote rejected] feature/fix -> feature/fix (pre-receive hook declined)
The Fix
The fix depends entirely on what the hook is checking. Read the remote error message carefully — well-written hooks print exactly what failed.
Commit message linting failure –> Amend commit: git commit –amend -m ‘PROJ-123: fix message’
Secret scanning failure –> Remove secret, use git filter-repo, rotate the credential
Branch naming policy failure –> Rename: git branch -m old-name feat/PROJ-123-description
File size limit failure –> Use Git LFS or remove file from history with git filter-repo
Signed commit required –> Configure signing: git config commit.gpgsign true
For CI/CD pipelines, configure the pipeline git user to produce valid commit messages automatically:
BRANCH=${{ github.head_ref }}
TICKET=$(echo $BRANCH | grep -oE ‘[A-Z]+-[0-9]+’)
git commit -m “${TICKET}: Automated release commit [skip ci]”
7. Cause #5 — SSH Key / Token Auth Failures
What Is Happening
Authentication failures are frequently misdiagnosed as push rejections because the error appears at different points. The push is rejected not because of a Git policy, but because the server cannot verify who you are.
Permission denied (publickey).
fatal: Could not read from remote repository.
Please make sure you have the correct access rights and the repository exists.
In CI/CD environments, this typically happens when:
- The SSH deploy key was rotated but the secret in the pipeline was not updated
- The GITHUB_TOKEN in GitHub Actions does not have write permission for the repository
- You are using a personal SSH key tied to an individual account that gets deleted when they leave the org
- The known_hosts file is missing in a Docker-based CI runner
The Fix
Diagnose SSH connectivity:
ssh -vT git@github.com
ssh -i ~/.ssh/deploy_key -T git@github.com
Fix GITHUB_TOKEN permissions in GitHub Actions:
permissions:
contents: write # Required for git push
pull-requests: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
Fix known_hosts in Docker CI runner:
– name: Setup SSH known hosts
run: |
mkdir -p ~/.ssh
ssh-keyscan -H github.com >> ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
8. Cause #6 — Wrong Remote URL or CI Service Account
What Is Happening
Sometimes the push rejection is caused by something simpler: the remote URL is wrong, or the service account used by the CI runner does not have access to the repository it is trying to push to. This is especially common in:
- Monorepo setups where multiple repos share CI configuration
- Org-to-org forks where the remote URL still points to the fork
- Repository renames or transfers that were not updated in pipeline configs
The Fix
# Check current remote URL
git remote -v
# Fix wrong remote URL
git remote set-url origin https://github.com/prgxlabs/correct-repo.git
# Verify service account has access
curl -H ‘Authorization: token YOUR_TOKEN’ \
In GitLab CI/CD, verify that the CI_JOB_TOKEN or deploy token has developer or maintainer role on the target project.
9. Quick Reference: Error Messages and Fixes
Use this table as your first diagnostic step whenever you see a git push rejected error.
protected branch hook declined –> Branch protection rule –> Grant bypass to service account or push via PR
non-fast-forward –> Diverged history –> git fetch + rebase, or –force-with-lease
force push is not allowed –> Force push blocked –> Delete and recreate tag, or use admin override
pre-receive hook declined –> Server-side hook failure –> Fix commit message, remove secrets, check branch name
Permission denied (publickey) –> SSH auth failure –> Update deploy key, check known_hosts
returned error: 403 –> Token permission issue –> Add contents: write permission to token
would clobber existing tag –> Tag already exists –> Delete tag first, then re-push
Repository not found –> Wrong remote URL or no access –> git remote set-url origin, verify service account
10. Best Practices to Prevent Push Rejections in CI/CD
Use GitHub Apps Instead of PATs
GitHub Apps generate short-lived installation tokens automatically scoped to the repositories you install them on. They eliminate the ‘token expired at 3 AM’ class of incidents.
Separate Push Steps from Deploy Steps
In the case study, the push failure caused a rollback of an already-successful deployment. The fix was to separate the pipeline into two independent jobs: one for deploying the application, one for updating Git refs. A failure in the tag-push job no longer triggers the application rollback.
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps: […]
tag-release:
needs: deploy
runs-on: ubuntu-latest
if: success()
steps:
– name: Push release tag
continue-on-error: true # Tag push failure does not roll back deploy
run: git push origin v${{ env.VERSION }}
Validate Commit Messages Before They Reach the Remote
Do not wait for the server-side pre-receive hook to reject commits. Use pre-commit hooks locally and in CI to catch issues early:
# .pre-commit-config.yaml
repos:
– repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.2.0
hooks:
– id: conventional-pre-commit
stages: [commit-msg]
Use Concurrency Guards in GitHub Actions
Prevent two pipeline runs from racing to push to the same branch:
concurrency:
group: production-${{ github.ref }}
cancel-in-progress: false
Add a Dry-Run Pre-Check Step
Add git push –dry-run origin main as a step before your actual push. This runs all the server-side checks without actually pushing, letting you catch rejections before they block your deploy.
11. Frequently Asked Questions
What is the difference between ‘git push rejected’ and ‘git push failed’?
A rejected push means the remote server received your push request but deliberately refused it based on a policy or check. A failed push means the connection or authentication broke before the server could even evaluate your request. Rejected shows [rejected] or [remote rejected] in the ref status. Failed shows fatal: or error: at the connection level.
Can I bypass branch protection rules with git push –force?
No. Branch protection rules are enforced server-side, and –force only overrides the non-fast-forward local check. If the server’s protection rule blocks force pushes, –force will still be rejected. You need either bypass permissions or an admin to temporarily disable the rule.
Why does git push work locally but fail in CI/CD?
Almost always an auth issue. Your local git uses your personal SSH key or cached credentials, while CI uses a service account token or deploy key. Check that the CI token has write access to the repo, has not expired, and is loaded correctly in the pipeline environment. Also verify the remote URL — CI runners often use HTTPS remotes while developers use SSH.
How do I push to a protected branch in GitHub Actions?
Add the GITHUB_TOKEN or PAT to the checkout step and set contents: write in the workflow permissions block. If the branch requires PR reviews, you need to either grant the service account bypass permissions in the branch protection rule, or restructure the pipeline to push to a staging branch and auto-merge via the GitHub API.
What is –force-with-lease and when should I use it?
–force-with-lease is a safer alternative to –force. It will only force-push if the remote ref is at the same SHA as when you last fetched. If someone else pushed a commit between your fetch and your push, the force-with-lease will be rejected, protecting you from accidentally overwriting their work. Always prefer –force-with-lease over –force in automated pipelines.
How do I debug a pre-receive hook rejection when I cannot see the hook script?
The hook’s stderr output is forwarded to your terminal as remote: lines. Read these carefully — most well-written hooks include actionable error messages. If the message is unclear, contact your Git server admin. For GitHub/GitLab SaaS, pre-receive hooks are replaced by push rules and protected branch rules, which are fully documented in the platform UI.
Additional Resources
- Troubleshooting GIT
- Git Branching Explained: Internals, Strategies, and Real-World Examples (2026 Guide)
- How to Fix Git Merge Conflicts (Step-by-Step Guide for Developers in India & Worldwide) [2026]
Conclusion
A git push rejected error is almost never a random failure. Every rejection has a deterministic cause, and once you know how to read the error message, the fix is usually straightforward.
To summarize the playbook from this case study:
- Read the error message in full — the first line after [remote rejected] tells you the category
- Check branch protection rules and service account permissions first — they are the most common cause in modern CI/CD
- Use –force-with-lease, never –force, when rewriting history is genuinely necessary
- Separate your deployment jobs from your Git ref update jobs so one failure cannot cascade
- Run git push –dry-run in a pre-check step to catch rejections before they block deployments
If you have hit a git push rejected scenario that is not covered here, drop it in the comments below. I am curious what edge cases teams are running into in production CI/CD pipelines.
About the Author
Kedar Salunkhe
DevOps Engineer | Seven years of fixing things that break at 2am
Kubernetes • OpenShift • AWS • Coffee
I’ve spent almost 7 years keeping production systems running, often when everyone else is asleep. These days I’m working with Kubernetes and OpenShift deployments, automating everything that can be automated, and occasionally remembering to document the things I fix. When I’m not troubleshooting clusters, I’m probably trying out new DevOps tools or explaining to someone why we can’t just “restart everything” as a debugging strategy. You can usually find me where the coffee is strong and the error logs are confusing.