1. 先搞懂错误链路:husky、Git hooks 与退出码
1.1 pre-commit hook 到底什么时候触发
用过 Husky 的人都清楚,它本质上是帮你把 Git 的钩子脚本管理起来的一个工具。Git 本身自带一套事件机制,比如 pre-commit、pre-push、commit-msg 等等,每次触发 git commit 的时候,Git 会先去 .git/hooks/ 目录下找对应的可执行脚本,如果有就执行,执行完再看退出码决定是否继续。
Husky 做的事情其实并不玄乎:它在安装阶段把自身注册到 .git/hooks/ 的各个钩子脚本里,然后在用户侧的 package.json 或 .husky/ 目录中读取你自定义的命令,按钩子类型去执行。pre-commit hook exited with code 1 这个报错,字面意思就是:提交前的钩子已经跑了,但脚本最终返回了退出码 1,也就是失败,Git 因此拒绝本次提交。
一个常见的误解是:只要在终端里把 lint 跑过了,pre-commit 就不会出问题。实际上 pre-commit 钩子里跑的是一套全新的、独立于你手动终端的命令序列,它可能包含 ESLint、Prettier、TypeScript 类型检查、单元测试甚至自定义脚本。任何一个环节返回非 0,Git 都会把这笔账算到 pre-commit hook 头上,最终显示就是这句令人头皮发麻的报错。
1.2 退出码 1 到底在说什么
在 Linux / Unix 的进程模型里,退出码 0 表示成功,非 0 都表示异常。Git 对钩子脚本的要求也一样:钩子执行完如果返回值不是 0,就中止当前操作。exited with code 1 表示钩子的最后一条命令返回了 1,但注意,这并不等于钩子的“第一行命令”失败了。
实际操作中,你会看到这样的情况:husky 的输出里可能有几段 ESLint 的输出、一段 Prettier 的输出,甚至还有一段 git diff 的结果,最后才来一句 error Command failed with exit code 1。如果你着急去看最后一行,可能根本不知道真正挂掉的是哪一个命令。我习惯的做法是先看完整输出,尤其注意每个工具自己打印的 error 字样,那才是真正的失败源。
还有一点值得说:退出码 1 是绝大多数命令行工具在“有错误发生”时返回的通用值,ESLint、Prettier、Jest 都会这样。所以你不能只凭退出码判断问题性质,必须结合具体的 stdout/stderr 内容。这个习惯在调试任何 CI/CD 流程时都一样,看日志永远先看“谁报的错”,而不是只看“整体退出码”。
1.3 为什么“完备的 lint 设置”仍然会失败
我自己被这个东西坑过很多次,总结下来,pre-commit hook exited with code 1 高频触发场景大致有这几类:
- 代码里有真实的 lint 错误,比如未使用的变量、类型不匹配、尾逗号缺失。
- 格式化和 lint 规则冲突,Prettier 改了文件,ESLint 又不认,两个工具来回打架。
- husky 配置的脚本写错了,比如命令不存在、路径不对、可执行权限缺失。
- lint-staged 过滤范围不对,它尝试去处理了不该处理的文件,比如二进制资源或生成文件的 diff。
- TypeScript 的
tsc --noEmit检查覆盖了整个项目,而不仅仅是暂存区文件,导致“别人的报错”挡在了你的提交前面。 - 缓存和环境问题,比如 node_modules 损坏、husky 版本升级后钩子没重新注册。
这些原因里的前两个是良心报错,说明你的代码规范检查正在起作用,改代码就行。后面几个才是真正让人想拍桌子的,因为报错信息绕来绕去,最后可能指向一个根本不是“代码质量”的问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
