1. 从一次真实的 push 被拒说起:pre-receive hook declined 到底是什么
git push敲下去,终端没有像往常一样滚动出Writing objects的进度条,而是直接甩回来一行红字:
remote: GitLab: You are not allowed to push code to protected branches on this project. ! [remote rejected] main -> main (pre-receive hook declined) error: failed to push some refs to 'git@gitlab.example.com:team/repo.git'如果你第一次遇到pre-receive hook declined,很容易以为是网络问题或者本地仓库坏了,于是反复git push、删掉远程重新添加、甚至重装 Git,结果报错一字不变。其实这个报错跟网络、跟本地仓库状态基本无关,它是服务端主动拒绝你的推送。
pre-receive是 Git 服务端的一个钩子(hook)。当你的git push把对象传到服务端后,服务端在真正更新分支引用之前,会先运行这个钩子脚本。脚本会拿到你这次推送涉及的所有引用变更,逐条校验:你有没有权限推这个分支、这个分支是不是受保护的、提交信息是否符合规范、有没有大文件、有没有触发 CI 门禁。只要有一条不通过,脚本返回非零退出码,Git 就会把整个推送回滚,并告诉你pre-receive hook declined。
所以这个报错是一个笼统的“服务端校验失败”信号,真正的原因藏在remote:开头的那几行提示里。排查的核心思路就是:先读remote:日志定位是哪一类校验挂了,再对症处理。常见的成因可以归成三类:分支保护规则拦截、账号 push 权限不足、hook 脚本自定义校验(提交信息格式、文件大小、签名等)不通过。
这篇文章面向的是本地推送失败、想快速定位并恢复推送的开发者。我会按“读日志 → 查权限 → 查分支保护 → 查 hook 脚本 → 用统一通道验证”的顺序,把每一步的可复制命令和判断依据都写清楚。顺带记录一下我在用 TaoToken 统一管理 API Key 和 Base URL 时,怎么把鉴权配置和 Git 推送排查串起来,避免在多个工具之间来回切换配置。
先明确一点:pre-receive hook declined不是 Git 客户端的问题,你在本地怎么折腾都没用。必须去服务端侧找原因。下面进入具体排查。
2. 先读 remote 日志再动手:定位 pre-receive hook declined 真实成因
很多人一看到报错就去搜“pre-receive hook declined 怎么解决”,搜到的答案五花八门,因为不同平台、不同 hook 脚本给出的原因完全不同。正确的第一步永远是:把完整的报错日志读全。
2.1 用 GIT_TRACE 和 verbose 拿到完整服务端输出
默认情况下,Git 会把服务端返回的remote:行打印出来,但有时候被截断或者你滚动太快没看清。可以加上 verbose 参数重推:
GIT_TRACE=1 GIT_CURL_VERBOSE=1 git push origin main 2>&1 | tee push.log如果你用的是 SSH 协议,GIT_CURL_VERBOSE不生效,改用:
GIT_TRACE_PACKET=1 GIT_TRACE=1 git push origin main 2>&1 | tee push.log然后重点看push.log里所有以remote:开头的行。举几个真实例子:
remote: GitLab: You are not allowed to push code to protected branches on this project.这是 GitLab 的分支保护拦截。
remote: error: GH006: Protected branch update failed for refs/heads/main. remote: error: Required status check "ci/build" is expected.这是 GitHub 的分支保护 + 必需状态检查未通过。
remote: Commit message does not match the required pattern: ^(feat|fix|docs|chore)(\(.+\))?: .+这是自定义 hook 在校验提交信息格式(Conventional Commits)。
remote: File build/app.apk is 120.00 MB; this exceeds GitHub's file size limit of 100.00 MB这是大文件拦截。
2.2 判断是“权限类”还是“规则类”
读完日志后,先做一个粗分类,这决定了你接下来查哪里:
| remote 日志关键词 | 成因类别 | 处理方向 |
|---|---|---|
| not allowed to push / protected branch | 分支保护 | 平台侧调整保护规则或加白名单 |
| permission denied / you don't have permission | 账号权限 | 找管理员加 Developer/Maintainer 角色 |
| does not match the required pattern | hook 脚本校验 | 改提交信息或调整 hook |
| exceeds the file size limit | hook 脚本校验 | 移除大文件、用 LFS |
| required status check | CI 门禁 | 等 CI 通过或调整规则 |
如果日志里只有一句干巴巴的pre-receive hook declined,没有任何remote:提示,那说明服务端的 hook 脚本没有输出友好信息,或者输出被平台吞了。这时候你需要联系仓库管理员,让他去服务端看 hook 脚本的日志(通常在仓库的hooks/pre-receive或平台的审计日志里)。
2.3 确认你推的是哪个分支、哪个引用
有时候你以为自己在推feature/xxx,实际上本地main也一起被推上去了。用下面命令确认这次推送涉及哪些引用:
git push --dry-run origin main git rev-parse --abbrev-ref HEAD git branch -vv--dry-run会模拟推送但不真正传对象,能提前暴露引用范围。如果发现本地main落后于远程、或者你误把main也带上了,先切到正确的分支再推:
git checkout feature/my-change git push origin feature/my-change这一步能解决相当一部分“我明明推的是自己的分支”的困惑。确认清楚引用之后,再进入权限和分支保护的排查。
3. 分支保护与 push 权限排查:可复制的 git 配置与平台操作
定位到是分支保护或权限问题后,处理分两条线:平台侧调整规则,本地侧确认身份和远程地址。平台侧的操作各平台不同,但本地侧的命令是通用的。
3.1 确认本地提交身份和远程地址
先确认你用的账号是不是有权限的那个:
git config --get user.name git config --get user.email git remote -v如果user.email跟你平台账号绑定的邮箱不一致,服务端可能识别不出你是谁。修正:
git config --global user.name "your-name" git config --global user.email "your-email@example.com"远程地址也要确认协议和主机对不对:
git remote set-url origin git@gitlab.example.com:team/repo.git3.2 分支保护的处理方式
分支保护是pre-receive hook declined最常见的成因。以 GitLab 为例,进入Settings → Repository → Protected branches,你会看到main、release/*这类分支被设为Protected,允许推送的角色通常是Maintainer及以上。如果你只是Developer,推送就会被拒。
三种处理方式,按推荐顺序:
第一种,把自己的分支推成新分支,走 Merge Request。这是最规范的做法,不破坏保护规则:
git checkout -b feature/fix-push-issue git push origin feature/fix-push-issue然后在平台上发起 MR,由有权限的人合并。
第二种,让管理员把你的账号加入白名单。GitLab 的保护分支设置里有Allowed to push一栏,可以把特定用户加进去,这样你就能直接推main。
第三种,临时取消分支保护。仅建议在个人项目或紧急情况下使用,团队项目慎用,因为保护规则本身是为了防止误推和绕过 CI。
GitHub 的对应位置在Settings → Branches → Branch protection rules,可以设置Require a pull request before merging、Require status checks to pass、Restrict who can push。如果你被Restrict who can push拦了,同样需要管理员把你加进允许列表。
3.3 用统一配置管理多仓库鉴权
当你在多个仓库、多个平台之间切换时,鉴权配置容易乱。我习惯用一个统一的 API 通道来管理 Key 和 Base URL,减少在.gitconfig、环境变量、CI 配置之间反复改的麻烦。TaoToken 提供统一的 Base URL 和 Key,配置一次就能在多个工具里复用。
在项目里放一个.env或者本地配置文件,把通道信息集中管理:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-your-taotoken-key", "model_id": "claude-sonnet-4-5", "timeout": 60 }如果你用的是 Claude Code 这类工具,配置通常落在~/.claude/settings.json或项目级settings.json:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-your-taotoken-key", "ANTHROPIC_MODEL": "claude-sonnet-4-5" } }注意这里的三件套必须齐全:Base URL + Key + Model ID。少任何一个,工具要么报 401,要么报 model not found。Base URL 用https://taotoken.net/api,不要带多余的路径后缀。
如果你用 Codex,配置落在~/.codex/auth.json:
{ "OPENAI_BASE_URL": "https://taotoken.net/api", "OPENAI_API_KEY": "sk-your-taotoken-key", "OPENAI_MODEL": "gpt-5" }Cline 这类 VS Code 插件则在设置面板里填 Base URL、API Key、Model ID 三项。Cline 支持 MCP,但注意不要让 MCP 直连生产数据库,MCP server 的配置里只挂只读或测试环境。
把这些配置集中管理的好处是:当你在排查 Git 推送问题时,如果同时需要调用模型做代码审查或生成提交信息,不用再单独配一套鉴权。统一通道减少了“这个工具能连、那个工具连不上”的排查成本。
4. 验证请求与成功结果:一次完整的 push 恢复动作
配置和规则都调整完之后,需要一次完整的验证动作,确认推送真的恢复了。下面是我实际用的一套流程。
4.1 推送前的自检
git status git log --oneline -5 git fetch origin git rebase origin/maingit fetch+git rebase是为了确保你的本地分支基于最新的远程分支,避免因为落后太多被服务端拒绝(有些 hook 会校验是否 fast-forward)。
4.2 执行推送并观察输出
git push origin feature/fix-push-issue成功的输出应该类似:
Enumerating objects: 12, done. Counting objects: 100% (12/12), done. Delta compression using up to 8 threads Compressing objects: 100% (7/7), done. Writing objects: 100% (7/7), 1.23 KiB | 1.23 MiB/s, done. Total 7 (delta 3), reused 0 (delta 0), pack-reused 0 remote: remote: To create a merge request for feature/fix-push-issue, visit: remote: https://gitlab.example.com/team/repo/-/merge_requests/new?merge_request%5Bsource_branch%5D=feature%2Ffix-push-issue remote: To gitlab.example.com:team/repo.git * [new branch] feature/fix-push-issue -> feature/fix-push-issue关键看最后一行有没有[new branch]或main -> main,以及有没有remote rejected。如果remote:行变成了创建 MR 的提示,说明推送已经通过 pre-receive 校验。
4.3 用 API 通道做一次连通性验证
如果你在排查过程中同时配置了 TaoToken 通道,可以顺手验证一下通道是否正常。用 curl 发一个最小请求:
curl -s -X POST https://taotoken.net/api/v1/messages \ -H "Content-Type: application/json" \ -H "x-api-key: sk-your-taotoken-key" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-sonnet-4-5", "max_tokens": 64, "messages": [{"role": "user", "content": "reply with ok"}] }'正常返回会包含"content"字段和模型输出。如果返回 401,说明 Key 不对;如果返回 404,说明 Base URL 或路径不对;如果返回 model not found,说明 Model ID 写错了。这三类错误和 Git 推送的排查逻辑其实一样:先看返回信息,再定位是鉴权、地址还是参数问题。
4.4 确认远程分支状态
推送成功后,确认远程分支确实更新了:
git ls-remote origin feature/fix-push-issue git log origin/feature/fix-push-issue --oneline -3git ls-remote会返回远程引用的 SHA,跟你本地git rev-parse HEAD对比,一致就说明推送落地了。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
排查过程中会遇到一些高频报错,这里逐个对照。
5.1 401 Unauthorized
remote: HTTP Basic: Access denied fatal: Authentication failed for 'https://gitlab.example.com/team/repo.git'这是 Git 层面的 401,通常是用 HTTPS 协议时密码或 token 过期。处理:
git config --global credential.helper store git remote set-url origin git@gitlab.example.com:team/repo.git改用 SSH 协议可以绕开 HTTPS 的 token 过期问题。如果是 API 通道的 401,检查x-api-key或Authorization头是否带对,Key 有没有多余空格。
5.2 local proxy failed
fatal: unable to access 'https://...': Failed to connect to 127.0.0.1 port 7890: Connection refused这是本地代理配置残留导致的。检查 Git 的代理设置:
git config --global --get http.proxy git config --global --get https.proxy如果有输出,清掉:
git config --global --unset http.proxy git config --global --unset https.proxy同时检查环境变量HTTP_PROXY、HTTPS_PROXY是否指向了一个已经关闭的本地端口。这类问题在切换网络环境后特别常见。
5.3 reading choices 相关报错
error: reading choices: unexpected end of JSON input这类报错通常出现在调用模型 API 时,返回体不是合法 JSON。原因可能是 Base URL 配错,请求打到了错误的路径,返回了 HTML 错误页。检查 Base URL 是否为https://taotoken.net/api,不要多加/v1或/chat/completions之类的后缀,具体路径由工具自己拼接。
5.4 OAuth 相关报错
OAuth token expired, please re-authenticate如果你用的是 Claude Code 或 Codex 的 OAuth 登录方式,token 过期后需要重新登录。但如果你已经切换到 API Key 模式,就不应该再走 OAuth。检查配置文件里是否同时存在 OAuth 和 API Key 两套凭据,冲突时以 API Key 为准,把 OAuth 相关字段删掉。
5.5 三件套缺失导致的连锁报错
不管是 Git 平台还是 API 通道,配置类问题的根源往往是“三件套”不全。对照检查:
| 组件 | Git 场景 | API 通道场景 |
|---|---|---|
| 地址 | remote URL | Base URL |
| 凭据 | SSH Key / Token | API Key |
| 目标 | 分支名 | Model ID |
任何一项缺失或写错,都会表现为“连不上”或“被拒绝”。排查时按这个表逐项核对,比盲目搜索报错信息高效得多。
6. 把排查路径固化成习惯:统一通道与快速恢复
pre-receive hook declined本身不可怕,可怕的是没有排查路径,每次遇到都从头搜一遍。把上面的流程固化成习惯:先读remote:日志分类,再查分支保护和权限,然后确认三件套配置,最后做一次完整推送验证。
对于经常在多个仓库、多个工具之间切换的开发者,我建议把鉴权和通道配置集中管理。TaoToken 的统一 Base URL 和 Key 可以同时服务于 Git 辅助工具、代码审查、提交信息生成等场景,减少重复配置。需要新建 Key 或查看用量时,去控制台操作;想先验证模型通道是否正常,可以用模型对话页面发一条测试消息;如果是长期编码或 Agent 场景,Coding Plan 会更合适。
具体入口:
- 新建和管理 API Key:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite
- 接入文档和参数说明:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
- 模型对话验证通道:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite
- 长期编码与 Agent 场景:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite
- 控制台查看用量:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
最后留一个我踩过的坑:有一次推送被拒,日志只显示pre-receive hook declined,没有任何remote:提示。折腾了半天权限和分支保护都没问题,最后发现是服务端 hook 脚本里有一条“提交必须带 Jira 单号”的自定义校验,而我的提交信息里没写。所以当常规排查都走不通时,直接找仓库管理员看 hook 脚本内容,往往是最快的路径。