1. 先搞清楚 pre-receive hook declined 到底拦了什么
! [remote rejected] master -> master (pre-receive hook declined)这个报错,本质上是服务端在收下你的提交之前,跑了一段钩子脚本,脚本返回了非 0 退出码。注意关键词是 remote rejected,说明拒绝动作发生在远端,不是你本地git commit出了问题。很多人第一反应是「我是不是没权限」,其实权限只是四种常见成因之一,另外三种分别是服务端钩子校验失败、分支保护规则拦截、提交信息不符合规范。
我先把这条链路拆开讲清楚,你后面排查才不会乱。Git 推送的流程大致是:本地git push→ 建立连接 → 服务端先跑pre-receive钩子(在任何一个 ref 更新前执行)→ 再跑update钩子(每个 ref 单独执行)→ 最后跑post-receive。只要pre-receive返回失败,整批推送全部回滚,客户端看到的就是pre-receive hook declined。所以这个报错是个「大帽子」,底下可能压着完全不同的原因。
适合谁看这篇:正在往 master/main 推代码被拒的开发者、刚接手一个带 CI 钩子的仓库的新人、以及用 TaoToken 做模型接入联调时顺手把代码托管流程也理顺的同学。你不需要是 Git 专家,但需要能敲命令、能看日志。
先给你一个判断顺序,别一上来就改分支:
| 现象 | 大概率成因 | 先查什么 |
|---|---|---|
| 只有 master 被拒,新分支能推 | 分支保护 | 仓库保护规则 |
| 所有分支都推不上去 | 权限 / 账号 | 远程地址与凭据 |
| 报错前有钩子输出文字 | 服务端钩子校验 | 钩子日志/CI 输出 |
| 提交信息格式被点名 | commit message 规范 | 本地 commit 内容 |
这里有个容易踩的坑:很多人看到「没权限」就去新建分支,结果新分支推上去照样被拒,因为钩子是对所有 ref 生效的。所以第一步永远是看服务端到底打印了什么,而不是猜。
我试过最省事的定位方式,是先把推送命令加上详细输出,让服务端钩子的 stdout/stderr 完整回显:
GIT_TRACE=1 GIT_CURL_VERBOSE=1 git push origin master 2>&1 | tee push.logtee把输出同时写进push.log,方便你回头搜关键字。如果钩子脚本里有echo "commit message must match ..."这类提示,它会出现在这份日志里,比干瞪眼强得多。
再补一个本地复现思路,方便你在自己可控的仓库里模拟这个错误,理解它的触发条件。找一个你有写权限的测试仓库,在服务端.git/hooks/pre-receive放一个永远失败的脚本:
#!/bin/sh echo "pre-receive hook: reject for demo" exit 1给执行权限后随便推一次,你就能稳定复现pre-receive hook declined。这个练习的价值在于:你会亲眼看到「拒绝发生在服务端」,从而不再把时间浪费在本地git status上。理解了这个机制,后面四个角度的排查就是按图索骥。
2. TaoToken 前置准备:把接入凭据和仓库凭据分开管
这一节不是让你去注册,而是讲清楚一个经常被混淆的点:代码仓库的推送凭据和模型服务的 API 凭据是两套东西,别混在一个环境变量里互相污染。你在排查 Git 推送问题时,如果同时在做模型联调,很容易把两边的 Key 搞串,最后报错信息对不上号。
TaoToken 这边你需要的是 API Key 和 Base URL,用于模型对话、Coding Plan 等调用场景。它的控制台入口在 https://taotoken.net/api ,API Keys 管理页在 https://taotoken.net/api-keys ,接入文档在 https://taotoken.net/doc 。这几个地址建议单独记,不要和 Git remote 地址写进同一个配置文件。
为什么要在 Git 排障文章里提这个?因为实际工作中,很多同学的~/.gitconfig、项目.env、shell profile 里塞满了各种 token,一旦推送报 401 或者钩子拒绝,根本分不清是哪个凭据在起作用。我的做法是物理隔离:
# 仓库凭据走 SSH,避免 HTTPS 反复弹窗 git remote set-url origin git@your-git-host:group/repo.git # 模型服务凭据只放在项目 .env,且 .env 进 .gitignore echo ".env" >> .gitignoreTaoToken 的调用侧配置,以 OpenAI 兼容方式为例,Base URL 填https://taotoken.net/api,Key 填你在控制台生成的令牌,模型 ID 按文档里列出的写。这三件套(Base URL + Key + Model ID)在任何客户端里都是成组出现的,缺一个就会报错。比如在 Cline 这类插件里配置 MCP 或模型时,三个字段必须同时填对,否则会出现reading 'choices'之类的解析错误——那类错误和 Git 钩子无关,但排查思路是一样的:先确认凭据三件套完整,再看服务端返回。
如果你用的是 Claude Code 这类编码工具,接入时同样遵循「Base URL + Key + Model ID」三件套原则,具体字段名以官方文档为准,不要凭记忆填。文档地址再放一次:https://taotoken.net/doc ,遇到字段不确定就回去对一遍,比在群里问快。
这里给一个环境变量模板,把两类凭据分开命名,避免手滑:
# ~/.bashrc 或项目 .env export GIT_SSH_COMMAND="ssh -v" # 排障时临时开详细日志 export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_API_KEY="sk-你的令牌" export TAOTOKEN_MODEL="按文档填写的模型ID"注意GIT_SSH_COMMAND只在排障时开,平时关掉,否则每次推送刷一屏日志。这个习惯能帮你在「到底是网络问题还是钩子问题」之间快速切换视角。
3. 可复制配置:四个角度逐项落地
这一节是全文的操作核心,按服务端钩子、分支保护、权限、提交规范四个角度给可复制的配置和命令。每一条都对应一种真实拦截原因,你可以按第 1 节的判断表挑着做。
3.1 服务端钩子:先拿到钩子输出再谈修复
pre-receive钩子通常由仓库管理员或 CI 系统部署,普通开发者看不到脚本内容,但能看到它的输出。所以你的动作是「让输出可见」,而不是「改钩子」。上面第 1 节的GIT_TRACE命令已经能拿到大部分输出。如果输出被吞了,试试服务端的另一种常见形态——钩子把详细原因写进了 CI 日志,这时你需要去仓库的流水线页面看对应 commit 的构建记录。
本地能做的验证是确认你的提交里有没有触发钩子的「敏感内容」,比如大文件、密钥、特定路径。用一个提交范围检查:
# 查看本次要推送的提交涉及哪些文件 git diff --stat origin/master..HEAD # 检查是否误提交了大文件(>5MB) git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | awk '/^blob/ && $3 > 5000000 {print $4, $3}'如果发现大文件或密钥,钩子拒绝就说得通了。处理方式是git rm --cached移除后重新提交,密钥还要立刻轮换。这一步做完再推,往往就过了。
3.2 分支保护:master 被单独拦下的典型
如果只有 master 推不上去、新分支正常,基本可以锁定分支保护。保护规则一般要求:不能直接 push、必须走 Merge Request、必须通过 CI、必须至少一个 approve。这些规则在服务端配置,你本地改不了,但可以按规则走流程。
可复制的操作是切功能分支并推送:
git checkout -b feature/fix-push-issue git add . git commit -m "fix: 调整推送流程并补充钩子排查说明" git push --set-upstream origin feature/fix-push-issue推送成功后去仓库页面发起合并请求。这里有个细节:--set-upstream只在第一次推新分支时需要,之后直接git push即可。如果你之前已经推过同名分支,用git push -u origin HEAD更稳,它自动取当前分支名。
3.3 权限:区分「读权限」和「写权限」
能 clone 不代表能 push。只读权限的账号 clone 下来一切正常,一推就remote rejected。验证方式是看远程地址协议和账号:
git remote -v ssh -T git@your-git-host # 看返回的账号是不是你以为的那个如果 SSH 返回的账号不对,说明本机有多把 Key,~/.ssh/config里 Host 匹配到了错误的身份。修法是给这个仓库单独指定 Key:
# ~/.ssh/config Host your-git-host HostName your-git-host User git IdentityFile ~/.ssh/id_ed25519_work IdentitiesOnly yesIdentitiesOnly yes是关键,它阻止 SSH 挨个试所有 Key,避免「试到一把有读权限没写权限的 Key」这种玄学问题。
3.4 提交规范:被钩子点名的 commit message
不少团队的pre-receive会校验 commit message 是否符合约定式提交(Conventional Commits),比如必须以feat:、fix:、docs:开头。你的提交信息如果是「update」「修改一下」,就会被拒。检查最近几条提交:
git log --oneline -5不符合就改写。注意已经推送过的提交不要随便 rebase 改写,会污染他人历史;只改写还没推上去的本地提交:
git commit --amend -m "fix: 修正推送被钩子拒绝的问题"多个本地提交要批量改,用交互式 rebase:
git rebase -i origin/master # 把需要改的提交前标为 reword,保存后逐个改信息3.5 一份可直接套用的 settings 片段
如果你在项目里用脚本统一管理推送前检查,可以把下面这段放进package.json的 scripts 或 Makefile,推之前跑一遍,把四类问题挡在本地:
{ "scripts": { "prepush:check": "git diff --stat origin/master..HEAD && git log --oneline origin/master..HEAD", "push:safe": "npm run prepush:check && git push origin HEAD" } }这段 JSON 的作用是把「看差异 + 看提交信息」固化成习惯,减少被钩子打回的次数。它不是银弹,但能让你在推送前就发现大文件、奇怪的文件名、不规范的提交信息。
4. 验证请求与成功结果:怎么确认真的通了
改完之后不能只看「没报错」,要确认远端状态确实更新了。分三步验证。
第一步,确认推送命令的返回。成功时你会看到类似这样的输出:
Enumerating objects: 12, done. Counting objects: 100% (12/12), done. Writing objects: 100% (7/7), 1.2 KiB | 300.00 KiB/s, done. To your-git-host:group/repo.git a1b2c3d..e4f5g6h feature/fix-push-issue -> feature/fix-push-issue关键是最后一行出现了->且没有rejected。如果走的是合并请求流程,推送成功只代表分支上去了,合并还要在页面操作。
第二步,用git ls-remote直接问服务端要 ref 列表,这是最硬的验证,不依赖本地缓存:
git ls-remote origin | grep feature/fix-push-issue能查到对应 commit hash,说明服务端确实收下了。查不到就是没推成功,回去看第 3 节。
第三步,如果你在联调模型服务,顺手验证一下 TaoToken 的调用链路是否也正常,避免「代码推上去了但配置没生效」。用 curl 发一个最小请求:
curl -s https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "'"$TAOTOKEN_MODEL"'", "messages": [{"role": "user", "content": "ping"}] }'返回里能看到choices数组就说明凭据三件套是对的。如果这里报 401,那是 Key 问题;报模型不存在,那是 Model ID 问题;和 Git 钩子完全是两条线,别互相甩锅。
成功结果的判断标准我总结成一句话:远端 ref 更新 + 本地日志无 rejected + 合并请求可创建。三者齐了才算真正解决,只满足一条可能是假象。
5. 本篇常见错排查:对照真实报错逐条拆
这一节按报错原文归类,你直接搜自己的报错关键字即可。
报错一:! [remote rejected] master -> master (pre-receive hook declined)这是本文主角。先按第 1 节拿钩子输出,再按第 3 节四角度排查。最常见的组合是「分支保护 + 提交信息不规范」同时命中,改完分支和 message 一起推就过了。
报错二:fatal: Authentication failed或401这不是钩子问题,是凭据问题。HTTPS 方式下检查是否用了过期 token,SSH 方式下用ssh -T看账号。注意 401 出现在模型服务调用里时,指的是 TaoToken 的 Key 无效,和 Git 无关,别混。修法是重新生成 Key 并更新到对应位置,Git 的凭据和模型的 Key 各管各的。
报错三:local proxy failed或连接超时这类错误发生在网络层,推送根本没到服务端,自然不会触发钩子。检查git config --get http.proxy是否残留了失效配置,有就清掉:
git config --global --unset http.proxy git config --global --unset https.proxy清完再推。如果公司网络有强制要求,按 IT 给的配置来,不要自己乱设。
报错四:reading 'choices'或返回体解析失败这是模型调用侧的典型错误,通常因为 Base URL 少了/v1或多了斜杠、Model ID 拼错、返回不是预期 JSON。对照文档把三件套重新核对一遍。它和 Git 推送无关,但经常在同一篇排障里被一起问到,所以列在这里做个切割。
报错五:OAuth相关跳转失败或授权回调错误出现在用 OAuth 方式登录代码托管平台或某些客户端时。检查回调地址是否和平台登记的一致,本地端口有没有被占用。这类问题优先看客户端日志里的完整 URL,别只看最后一行。
报错六:error: failed to push some refs to '...'这是pre-receive hook declined的伴随行,不是独立原因。看到它就往上翻,找remote rejected那行和钩子输出。
排查时的一个通用技巧:把报错原文整段复制去搜,比只搜关键词准。因为不同平台的措辞差异很大,整段匹配能直接命中同类案例。另外,改完配置后先推一个无关紧要的小提交验证,别拿重要分支试错。
6. 把流程固化下来:下次不再被同一个钩子拦住
真正省时间的做法不是每次报错再查,而是把检查前移。我的习惯是在项目根目录放一个scripts/pre-push-check.sh,内容就是第 3.5 节那套差异和提交信息检查,再配合 Git 自带的pre-push钩子自动执行:
#!/bin/sh # .git/hooks/pre-push remote="$1" url="$2" echo "检查待推送提交..." git log --oneline origin/master..HEAD git diff --stat origin/master..HEAD exit 0给执行权限chmod +x .git/hooks/pre-push,之后每次git push前都会先打印一遍要推的内容,大文件和奇怪提交一眼就能看到。这个钩子是本地钩子,不会被推送到远端,团队成员各自配置即可。
再进一步,把「master 不直接推」变成肌肉记忆:本地 master 只用来拉取,所有改动走功能分支。命令层面可以用别名降低心智负担:
git config --global alias.fb "checkout -b" # 用法:git fb feature/xxx对于经常做模型接入联调的同学,建议把 TaoToken 的接入文档和 API Keys 页面加入浏览器书签,遇到凭据类报错直接对照,不要凭记忆改配置。文档在 https://taotoken.net/doc ,Key 管理在 https://taotoken.net/api-keys ,模型对话入口在 https://taotoken.net/api 。这三个入口覆盖了「查文档、管凭据、验证调用」的完整闭环,和 Git 排障是两条并行的工作流,分开管理最省心。
最后留一个实用判断:如果同一个仓库、同一个账号,昨天能推今天不能推,优先怀疑服务端规则变更(钩子升级、保护规则收紧),而不是本地环境;如果换了机器或换了账号才出问题,优先查凭据和 SSH 配置。按这个二分法走,大多数pre-receive hook declined十分钟内能定位到具体原因。