An accidental Git push can expose unfinished code, trigger a deployment, bypass the intended review path, or send secrets to a remote. Local safeguards reduce mistakes, but repository rules and deployment approvals provide the stronger boundary.
Best baseline: keep the fork as
origin, keep the canonical repository as fetch-onlyupstream, require pull requests on protected branches, and let CI/CD deploy with a separate identity.
Make an upstream remote fetch-only
Git stores fetch and push URLs separately. Set the canonical upstream push URL to an intentionally invalid value while retaining its fetch URL:
git remote add upstream [email protected]:company/project.git
git remote set-url --push upstream DISABLED
git remote -v
Fetching from upstream still works. A push fails locally because the push destination is invalid. Use a clear value such as DISABLED so future maintainers understand the intent.
Alternative: redirect upstream pushes to your fork
Some fork workflows redirect an accidental upstream push to the contributor’s fork:
git remote set-url --push upstream [email protected]:your-user/project.git
This is less strict because a push still succeeds somewhere. A fetch-only upstream is easier to reason about when production safety matters.
Set a safer default push behavior
Configure Git to push the current branch to its upstream branch and avoid guessing a destination:
git config --global push.default simple
git config --global push.autoSetupRemote true
Review global settings before standardizing them across a team. The safest choice depends on whether branches track forks, central remotes, or multiple repositories.
Add a pre-push guard
A local pre-push hook can reject a protected remote or branch. Place an executable script at .git/hooks/pre-push, or distribute versioned hooks through a team tool:
#!/bin/sh
remote_name="$1"
if [ "$remote_name" = "upstream" ]; then
echo "Push blocked: upstream is fetch-only."
exit 1
fi
while read local_ref local_sha remote_ref remote_sha
do
case "$remote_ref" in
refs/heads/main|refs/heads/production)
echo "Push blocked: use a pull request for protected branches."
exit 1
;;
esac
done
exit 0
Client-side hooks can be skipped with --no-verify, may not be installed on every clone, and can be edited by the user. Treat them as helpful guardrails, not enforcement.
Enforce rules on the remote
Use GitHub rulesets or protected branches to require pull requests, approving reviews, status checks, resolved conversations, signed commits where justified, and restrictions on force pushes and deletion. Keep bypass permissions small and audited.
A local mistake should fail at the server even when a hook is absent. Apply rules to every important branch and review patterns so a newly named production branch is not left unprotected.
Separate code merge from production deployment
- Use a dedicated CI/CD identity with the minimum environment permissions.
- Require an approval for production where the business risk justifies it.
- Protect production secrets and prevent them from reaching pull-request jobs from untrusted forks.
- Pin or review deployment dependencies and actions.
- Record the artifact, commit, approver, time, and result.
- Support rollback to a known artifact instead of rebuilding unknown code during an incident.
Use clear remote and branch names
The names origin and upstream are conventions, not security controls. Document what each remote represents. Use branch names that express purpose, and show the current branch and remote in the terminal prompt where possible.
Verify the setup
git remote get-url upstream
git remote get-url --push upstream
git config --get push.default
git branch -vv
git push --dry-run upstream HEAD
A dry run is useful but not proof of server enforcement. Test the rules with a disposable branch or repository and confirm that direct pushes, force pushes, deletions, missing checks, and unauthorized deployments fail as expected.
If a bad push already happened
- Stop further deployment or merging and tell the repository owner.
- Identify the exact commits, branches, tags, releases, and environments affected.
- If a secret entered Git history, revoke and rotate it immediately. Removing the commit is not enough.
- Prefer a normal revert on a shared branch. Rewrite history only with explicit coordination.
- Validate production, create a recovery record, and strengthen the failed control.
Recommended control stack
- Personal safety: fetch-only upstream and clear remote names
- Team convenience: shared pre-push checks and documented workflow
- Repository enforcement: protected branches or rulesets
- Release safety: CI/CD identity, protected environment, approval, and rollback
- Governance: small bypass group, audit trail, periodic access review, and incident procedure
These practices are especially useful for WordPress plugin, theme, and platform teams. If you need a maintainable delivery workflow, see enterprise WordPress plugin development.
Primary sources
- Git remote documentation
- Git hooks documentation
- GitHub rulesets documentation
- GitHub protected branches documentation
Frequently asked questions
Keep upstream’s fetch URL and set its push URL to an invalid value. Enforce branch rules on the server as the final boundary.
No. Local hooks can be absent, changed, or skipped. Use them for convenience and rely on remote rules for enforcement.
It is a useful convention in fork workflows, but Git does not enforce it. Confirm remote URLs in every clone.
Require pull requests and status checks through repository rules, restrict force pushes and deletion, and limit bypass permissions.
It previews what Git intends to send, but it does not replace branch protection, review, or deployment controls.
Revoke and rotate it immediately, then coordinate history cleanup if required. Assume the secret may have been copied.





