1. 先搞清楚 hook declined 到底是谁在拒绝你
git push报error: hook declined的时候,很多人第一反应是网络问题或者仓库地址写错了,其实这个报错跟网络基本没关系。它的完整形态通常长这样:
remote: error: hook declined to update refs/heads/master To https://gitee.com/xxx/xxx.git ! [remote rejected] master -> master (hook declined) error: failed to push some refs to 'https://gitee.com/xxx/xxx.git'关键信息在remote:这一行。remote前缀说明这句话是服务端打印出来的,不是本地 Git 客户端。也就是说,你的 commit 已经成功打包、压缩、上传到了远端,服务端在准备更新refs/heads/master这个引用时,被一个叫pre-receive或update的钩子脚本拦下来了。
钩子(hook)是 Git 服务端在特定时机自动执行的脚本。pre-receive在所有引用更新前运行,update在每个分支单独更新前运行。它们返回非零退出码,Git 就会拒绝这次推送,并把脚本输出的内容透传给你。所以hook declined本质是一句「服务端规则不允许」,而不是「Git 坏了」。
典型触发场景有这么几类:邮箱隐私策略(最常见,比如 Gitee 默认不允许真实邮箱提交)、分支保护(main/master 禁止直接 push)、提交信息格式校验(要求带 issue 号)、文件大小或后缀限制、以及仓库配额或权限问题。这篇就按「先定位是哪类钩子 → 再对症修复 → 最后用 TaoToken 把 AI 辅助排查的配置骨架搭好」的顺序走一遍,命令都可以直接复制。
2. 前置准备:把排查环境和大模型接入一起搭好
排查钩子问题本身不需要任何外部服务,但如果你想用 AI 帮你读钩子脚本、生成修复命令、或者批量改写历史提交的邮箱,就需要一个稳定的模型调用入口。我这边习惯用 TaoToken 做统一接入,它兼容 OpenAI 风格的接口,改个base_url就能用,省得在多个平台之间来回切 Key。
TaoToken 的 API 地址是https://taotoken.net/api,官网入口在https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。你需要先去控制台创建一个 API Key,然后把它写进本地配置。下面这套骨架同时覆盖了「命令行工具」和「编辑器插件」两种用法,你可以按需取用。
先看环境变量方式,最通用:
# Linux / macOS export TAOTOKEN_API_KEY="sk-你的key" export OPENAI_BASE_URL="https://taotoken.net/api" export OPENAI_API_KEY="$TAOTOKEN_API_KEY"# Windows PowerShell $env:TAOTOKEN_API_KEY="sk-你的key" $env:OPENAI_BASE_URL="https://taotoken.net/api" $env:OPENAI_API_KEY=$env:TAOTOKEN_API_KEY如果你用的是支持config.toml的 CLI 工具(比如一些终端 AI 助手),配置骨架大概是这样:
# ~/.config/taotoken/config.toml [provider] name = "taotoken" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" [model] default = "claude-sonnet" timeout_seconds = 60 [request] max_retries = 3编辑器侧(以 VS Code 系插件常见的settings.json为例):
{ "ai.provider": "openai-compatible", "ai.baseUrl": "https://taotoken.net/api", "ai.apiKey": "${env:TAOTOKEN_API_KEY}", "ai.model": "claude-sonnet", "ai.requestTimeout": 60000 }注意:API Key 不要硬编码进提交到仓库的文件里。上面用
${env:...}或环境变量引用的方式,就是为了避免 Key 跟着代码一起被 push 上去——顺带说一句,把密钥误提交也是触发某些服务端钩子的常见原因。
配置好之后,你可以让模型帮你做一件很实用的事:把服务端返回的钩子报错原文贴给它,让它判断属于哪类规则。这比你自己翻文档快得多。
3. 可复制的钩子排查命令与修复动作
3.1 第一步:确认本地提交用的邮箱
绝大多数hook declined都跟邮箱有关。先在项目目录里执行:
git config user.email git config user.name如果输出的是你的真实邮箱(比如xxx@qq.com),而服务端开启了「不公开邮箱」策略,那基本就锁定原因了。再看一眼最近几条提交实际用的邮箱:
git log -3 --pretty=format:"%h %an <%ae>"注意user.email配置和已有 commit 里记录的邮箱可能不一致——配置是「以后提交用哪个」,log显示的是「已经提交进去的是哪个」。钩子校验的是后者。
3.2 第二步:拿到服务端给的匿名邮箱
Gitee 这类平台会为每个账号分配一个形如用户名@user.noreply.gitee.com的匿名邮箱。你可以在平台的「邮箱设置」页面找到它。拿到之后,本地改成这个:
git config --global user.email "你的匿名邮箱@user.noreply.gitee.com" git config --global user.name "你的用户名"但注意,改配置只影响新提交。如果被拒绝的那批 commit 已经用了真实邮箱,光改配置再 push 还是会失败,因为历史提交里的邮箱没变。这时候要改写历史:
# 只改写最近 1 条提交 git commit --amend --reset-author --no-edit # 改写最近 N 条(比如 5 条) git rebase -i HEAD~5 # 在编辑器里把需要改的行的 pick 改成 edit,保存退出 # 对每条执行: git commit --amend --reset-author --no-edit git rebase --continue如果整个仓库历史都要改,可以用filter-branch(老仓库慎用,会重写所有 commit hash):
git filter-branch --env-filter ' OLD_EMAIL="你的真实邮箱@qq.com" NEW_NAME="你的用户名" NEW_EMAIL="你的匿名邮箱@user.noreply.gitee.com" if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]; then export GIT_COMMITTER_NAME="$NEW_NAME" export GIT_COMMITTER_EMAIL="$NEW_EMAIL" fi if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]; then export GIT_AUTHOR_NAME="$NEW_NAME" export GIT_AUTHOR_EMAIL="$NEW_EMAIL" fi ' --tag-name-filter cat -- --branches --tags改完之后强制推送(前提是你确认这个分支没有别人在协作):
git push -u origin master --force-with-lease--force-with-lease比--force安全,它会在远端有你不知道的新提交时拒绝覆盖。
3.3 第三步:区分是不是分支保护
如果邮箱没问题,报错里可能带protected branch或you are not allowed to push字样。这时候去平台仓库设置里看「分支保护规则」,确认master/main是否被设为「禁止直接推送」。解决办法是走 Pull Request 流程,或者临时关闭保护(不推荐长期关)。
3.4 第四步:看钩子脚本本身
如果你有服务端权限(自建 GitLab/Gitea),可以直接读钩子脚本:
# 服务端仓库目录下 ls -la hooks/ cat hooks/pre-receive cat hooks/update脚本里通常会有明确的exit 1和echo提示,对照着看就知道校验了什么。没有服务端权限的话,就只能靠remote:那几行输出反推。
4. 验证请求:确认修复真的生效
改完邮箱或历史之后,别急着大推,先用一个小提交验证:
echo "test" >> .hook-test git add .hook-test git commit -m "test: verify hook pass" git push origin master如果这次 push 成功,说明钩子规则已经满足。然后删掉测试文件:
git rm .hook-test git commit -m "chore: remove test file" git push origin master想更直观地确认服务端到底在校验什么,可以把报错原文丢给模型分析。用 curl 直接调 TaoToken 的接口验证一下连通性:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet", "messages": [ {"role": "user", "content": "git push 报错 remote: error: hook declined to update refs/heads/master,remote 提示 Push will publish a hidden email,这是什么原因?"} ] }'返回正常的话,你会拿到一段结构化的原因分析和修复建议。这一步同时也验证了你的 Key、base_url、模型名三件套配置是否正确。如果返回 401,检查 Key;返回 404,检查base_url有没有多写或少写/v1;返回超时,检查网络出口。
5. 本篇常见错排查
报错一:改了user.email还是 hook declined。原因:只改了配置,没改历史提交。钩子校验的是 commit 对象里的邮箱,不是git config的值。用git log --pretty=format:"%ae"确认历史里到底是什么,然后按 3.2 改写。
报错二:--force-with-lease也被拒绝。原因:远端有你本地没有的提交,或者分支保护禁止 force push。先git fetch看看远端状态,确认没有协作者后再考虑临时关保护。
报错三:filter-branch报Cannot rewrite branches: You have unstaged changes。原因:工作区不干净。先git stash或提交,再执行改写。
报错四:curl 调 TaoToken 返回 401。原因:TAOTOKEN_API_KEY没导出,或者 shell 会话里变量没生效。用echo $TAOTOKEN_API_KEY确认,Windows 下注意 PowerShell 和 CMD 的变量语法不同。
报错五:模型名写错导致 404。原因:不同 provider 的模型命名不一样。TaoToken 的可用模型列表在文档里,别直接套用别家的名字。接入文档和模型清单可以在https://taotoken.net/api-keys和https://taotoken.net/doc找到对应入口。
报错六:push 卡在Writing objects很久然后 hook declined。原因:包太大,服务端有单次推送大小限制。可以分批推,或者检查有没有误提交大文件(git rev-list --objects --all | sort -k2 | tail)。
6. 把 AI 接入固化进你的 Git 工作流
排查完这一次,建议把配置沉淀下来,下次遇到类似问题直接复用。如果你经常需要 AI 辅助读报错、写 commit message、生成修复脚本,可以考虑用 Coding Plan 把调用额度固定下来,避免每次临时找 Key。入口在https://taotoken.net/coding-plan。
日常对话式排查用模型对话页就够了:https://taotoken.net/chat。需要管理多个 Key、区分项目额度的话,控制台在https://taotoken.net/console。如果你用 Claude Code 这类终端 Agent 工具,接入配置参考https://taotoken.net/claudecode。
最后留一个我踩过的坑:改写历史邮箱之后,本地和远端的 commit hash 会全部变化,如果这个分支已经有人拉过,务必提前同步,否则对方下次 pull 会撞上一堆冲突。个人仓库随便改,团队仓库先沟通。