Git 钩子返回非零提交被拦速查:pre-commit 失败、no-verify 代价与 CI 兜底
更新时间:2026-10-24。FAQ 速查页,面向
git commit敲下去被一段脚本输出挡回来、提交根本没生成的同学。钩子的机制很直白:.git/hooks/下的脚本退出码非零,Git 就中止当前操作——pre-commit 拦提交、pre-push 拦推送、commit-msg 拦提交信息。难点不在机制,在于「拦你的是谁」:是仓库自带的裸钩子、husky 这类框架管理的钩子、还是 lint/test 检查真的发现了问题。病因收敛在六根轴上:钩子脚本本身执行失败要看完整输出、钩子的运行环境与交互 shell 不同、husky 装了但钩子没挂上、路径含空格让脚本炸掉、图省事--no-verify绕过把烂提交推上线、以及本地钩子和 CI 校验不一致导致「本地能过线上挂」。顺一遍基本必中。如果是提交信息格式被服务端拦而不是本地钩子,转 Git push rejected 速查。
本文要回答的问题
- 提交被拦了,怎么知道是哪个钩子在拦、它到底报了什么错?
- 为什么 lint 在终端里手动跑能过,钩子里跑就失败?
- 克隆的仓库有 husky,提交时钩子却完全没触发,哪里没装?
- 项目路径含空格,钩子脚本莫名报错,怎么修?
--no-verify能跳过钩子,为什么说不该随手用?
一、报错原文与典型现象
钩子拦截的报错形态是「钩子名 + 脚本输出 + 中止提示」,提交不会被创建(远端地址以 git.example.com 占位,路径以 /proj/demo 占位):
# pre-commit 钩子拦下提交
$ git commit -m "fix login"
husky - pre-commit script failed (code 1)
> lint-staged
✖ eslint found some errors. Please fix them for the staged files.
src/app.js
12:5 error 'debug' is not defined no-undef# 裸钩子脚本报错
$ git commit -m "wip"
.git/hooks/pre-commit: line 3: ./scripts/check.sh: No such file
or directory
error: failed to execute hook
# commit-msg 钩子拦提交信息
$ git commit -m "fix"
invalid commit message format: missing type prefix
hint: commit-msg hook declined the commit读现象先分三层:husky - pre-commit script failed 说明钩子框架正常触发、是里面的检查(lint/test)真挂了;.git/hooks/pre-commit: line N: ... 是裸钩子脚本自身执行出错,检查可能根本没跑;commit-msg hook declined 是提交信息格式被拦,和代码质量无关。三层修法不同,先定性。
二、病因处置速查表
| 报错现象 / 场景 | 可能原因 | 快速验证 | 处置 |
|---|---|---|---|
| lint/test 错误清单 + 钩子 failed | 代码真有问题被检查抓到 | 手动跑钩子同款命令复现 | 按清单修代码再提交 |
| No such file or directory | 钩子引用的脚本路径失效 | 手动执行 .git/hooks/pre-commit 复现 | 修脚本路径或重新装钩子 |
| 终端能过钩子里挂 | 钩子环境 PATH 与交互 shell 不同 | 钩子里加 env 输出来对比 | 脚本内写绝对路径或显式 source 环境 |
| 提交直接成功无钩子输出 | husky 装了但钩子未安装 | 查 .git/hooks/ 有无 husky 生成的钩子 | 执行安装命令重建钩子 |
| 路径含空格脚本炸 | 脚本未给路径变量加引号 | 看报错行是否断在空格处 | 脚本里所有路径变量加双引号 |
| 同事能提交自己不能 | core.hooksPath 被改过 | git config core.hooksPath 查看 | 复位或对齐团队约定路径 |
三、排查决策图
下面这张图是标准排查路线,颜色按角色区分:红是报错入口与命中的根因,蓝是排查动作,绿是通过分支,橙是修复动作,紫是防复发沉淀。
读这张图抓分水岭:报错里躺着具体的 lint/test 错误清单,说明钩子工作正常、是代码的问题,走左支按清单修;报错是脚本路径找不到、命令不存在,或者钩子干脆没触发,说明钩子环境或安装有问题,走右支修环境与安装。两条支路都别用 --no-verify 当终点——绕过只是推迟爆炸,CI 兜底才是正解。
四、逐条排查展开
先搞清楚拦你的是哪个钩子、它说了什么。 Git 在提交生命周期里会依次跑 pre-commit、prepare-commit-msg、commit-msg、post-commit 等钩子,报错信息头部通常会带钩子名(husky 会明确打 husky - pre-commit script failed)。不确定时直接看 .git/hooks/ 目录:非 sample 后缀的文件就是生效的钩子,cat 出来看内容。也可以手动执行 .git/hooks/pre-commit 复现——钩子就是个普通脚本,离开 git 也能跑。
钩子里的检查挂了 ≠ 钩子坏了。 最常见的剧本:pre-commit 里挂着 lint-staged 跑 eslint,你的代码真有一个未定义变量,钩子忠实地拦下了。这类情况的正确动作是读错误清单修代码,不是琢磨怎么绕过。想确认「检查本身能不能过」,在终端里手动跑同款命令(如 npx lint-staged 或 npm run lint),结果一致就是代码问题。
钩子环境与交互 shell 不同,是「终端能过钩子里挂」的根因。 钩子是 Git 以非交互 shell 拉起的,不读你的 shell 配置文件,PATH 里常常没有 nvm 管理的 node、没有 Homebrew 的 bin 目录。于是终端里 npm 好好的,钩子里 npm: command not found。修法两档:钩子脚本头部显式补上环境(如 source nvm 初始化脚本);更稳的是用 husky 官方推荐的加载片段,或在脚本里用绝对路径。GUI 客户端(如某些图形化 Git 工具)触发的提交环境更精简,同样的问题更常见。
husky 装了但钩子没挂上。 husky 的工作方式是修改 core.hooksPath 指向它管理的目录,安装动作由 prepare 脚本触发。克隆仓库后如果只装依赖而包管理器跳过了生命周期脚本(或用了忽略脚本的安装参数),钩子就不会生成,提交时静悄悄没有任何检查——这不是「能提交」,是防线根本没架起来。验证:git config core.hooksPath 看指向、查 .husky/ 目录下有无钩子文件;修复:手动执行安装命令重建,再故意提交一个违规文件验证钩子真的拦。
路径含空格是钩子脚本的经典炸点。 项目放在带空格的目录下(如「我的 项目」这类命名),钩子脚本里 $PROJECT_DIR/scripts/check.sh 不加引号,shell 会把路径拆成两段,报 No such file or directory 且断点就在空格处。修脚本:所有变量引用加双引号 "$PROJECT_DIR/scripts/check.sh"。这类问题在 Windows 上更普遍,因为用户目录常带空格或中文。
--no-verify 的代价要想清楚。 git commit --no-verify 跳过全部本地钩子,一时爽,但钩子存在的意义是把质量问题挡在仓库外——绕过一次,烂代码就进了历史,后面要靠 CI 拦截或代码评审擦屁股。合理的例外场景只有两种:钩子本身坏了正在修、且 CI 有同款校验兜底;或提交的是与代码质量无关的内容(如纯文档、版本号 bump)。团队层面要把「什么情况下允许 no-verify」写进规范,否则它会从例外变成习惯。
CI 端重复校验是兜底层,不是可选层。 本地钩子是便利不是防线——任何人都能 --no-verify,任何人都能改自己机器上的钩子。真正的防线在 CI:lint、test、提交信息格式检查在服务端再跑一遍,挂了拒合入。本地钩子和 CI 校验要保持同款规则同版本,否则会出现「本地全过、线上全挂」的双标体验,那比没有钩子更折磨人。
五、规范做法与预防
- 钩子脚本进版本库:用 husky 等框架管理,钩子内容随仓库走、入代码评审,不依赖各人手工配置。
- 脚本内显式环境:钩子脚本头部加载必要的环境初始化,路径变量一律加引号,不假设 PATH。
- 克隆后必跑安装步骤:README 写清依赖安装会触发钩子挂载,验证方式是故意提交违规内容看是否被拦。
- CI 同款校验兜底:lint/test/提交信息格式在 CI 重跑,本地钩子只做快速反馈。
- no-verify 白名单化:规范里写清允许绕过的场景与审批口径,禁止随手绕过。
- 钩子执行时间设预算:pre-commit 控制在秒级,慢检查(全量测试)挪到 pre-push 或 CI。
这一篇在知识体系里的位置
本篇是 Git 排错速查簇的「钩子与提交拦截」入口篇:钩子里命令找不到与环境变量问题交叉参考 Git 配置解析失败速查;钩子过了但推送被服务端拒看 Git push rejected 速查;索引锁导致提交失败是另一条线,看 Git index lock 速查。「先定位是哪个钩子 → 分清代码问题还是环境问题 → CI 兜底防绕过」这条主线对一切钩子类报错通用。
一句话总结
Git 提交被钩子拦下,先读报错头定位是哪个钩子:错误清单挂在 lint/test 上是代码真有问题,按清单修;报 No such file 或命令找不到是钩子环境/路径问题,脚本补环境、路径加引号;钩子完全没触发是 husky 未安装或 hooksPath 被改,重装并验证。--no-verify 只在钩子本身故障且 CI 有同款兜底时使用,本地钩子是便利、CI 重复校验才是防线,二者规则必须同款。