Pre-push钩子通过读取stdin中的ref信息(每行格式:local_ref local_sha remote_ref remote_sha)并校验remote_ref与远程名,如匹配origin/main则exit 1中止推送;需区分首次推送、删除操作及非快进情形,并依赖本地部署,无法自动同步。

Pre-push钩子如何拦截分支推送
Git pre-push 钩子在本地执行 git push 后、数据真正发往远程前触发,此时已知目标远程名、引用列表(如 refs/heads/main)和对应提交哈希,但尚未传输任何对象。它通过标准输入接收推送信息,格式为每行 local_ref local_sha remote_ref remote_sha,其中 remote_sha 为远程当前值(首次推送时为 0{40})。钩子返回非零退出码即可中止整个推送操作。
阻止向 origin/main 直接推送的典型写法
常见需求是禁止开发者直接向 origin/main 推送,强制走 PR 流程。关键点在于:只检查推送目标是否匹配,不依赖本地分支名;需区分首次推送(remote_sha 全零)和覆盖推送(remote_sha 非零但本地更旧);避免误杀其他远程(如 upstream)。
示例脚本逻辑:
#!/bin/bash
while read local_ref local_sha remote_ref remote_sha; do
if [[ "$remote_ref" == "refs/heads/main" ]] && [[ "$2" == "origin" ]]; then
echo "❌ Push to origin/main is blocked. Use PR instead."
exit 1
fi
done
注意: 是 git push 命令传入的远程名(即 git push origin main 中的 origin),必须显式校验,否则会错误拦截 git push upstream main。
容易踩的坑:SHA 比较与空提交处理
仅靠 ref 名称拦截不够安全——有人可能用 git push origin +main 强制覆盖,或推送空提交(local_sha 为 0{40})。实际应结合 SHA 判断是否真有内容变更:
-
local_sha为0{40}表示删除引用,一般不应允许删main -
remote_sha为0{40}表示首次推送,需额外策略(如仅允许 CI 账号执行) - 若允许 fast-forward,需确保
local_sha是remote_sha的后代(可用git merge-base --is-ancestor)
简单增强版判断:
if [[ "$remote_ref" == "refs/heads/main" ]] && [[ "$2" == "origin" ]]; then
if [[ "$local_sha" == "0000000000000000000000000000000000000000" ]]; then
echo "Deleting origin/main is not allowed"
exit 1
fi
if [[ "$remote_sha" != "0000000000000000000000000000000000000000" ]] && \
! git merge-base --is-ancestor "$remote_sha" "$local_sha"; then
echo "Non-fast-forward push to origin/main is blocked"
exit 1
fi
fi
部署时必须手动启用且无法强制同步
pre-push 是纯本地钩子,存于 .git/hooks/pre-push,不会随克隆传播。团队必须各自安装,或借助工具(如 husky)包装成 npm script 触发。CI 环境通常禁用钩子(git config --global core.hooksPath /dev/null),所以该机制只约束开发者本地行为,不影响自动化流程。真正的防护仍需靠远程服务端钩子(如 GitHub Branch Protection)或权限控制。
别指望靠这个“一劳永逸”——它只是第一道提醒,不是防线。漏装、绕过 git push --no-verify、或直接改远程 ref,都能跳过它。


















