1. 先搞清楚报错的来龙去脉
1.1 husky 是什么,pre-commit 又是什么
先说结论:这条报错不是 git 本身坏了,而是 git 在提交时触发了一个“钩子”,钩子里的检查脚本没跑通,返回了非 0 的退出码,git 出于安全考虑,直接终止了这次 commit。
git 的钩子机制(hooks)是原生就有的,它会在特定时机执行一些脚本,比如 commit 之前、push 之前、merge 之后等等。钩子文件默认放在 .git/hooks 目录下,里面是一堆带 .sample 后缀的示例脚本,正常情况下它们不会被触发。问题是手写钩子太麻烦,不同成员的开发环境复制起来也容易丢,于是 husky 就出现了。
husky 做的事情很简单:把钩子的管理从 .git 目录里解放出来,放在项目根目录的 .husky 文件夹下,并且可以随着仓库一起提交,团队成员拉下来代码之后通过 npm install 自动完成 hook 的注册和更新。.husky/pre-commit 文件就是我们今天要排查的主角,它一般长这样:
bash复制#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint
npx lint-staged
这个脚本的意思是:在你执行 git commit 之后、真正生成提交之前,先跑一遍代码检查。如果检查不过,脚本以非 0 状态退出,git 就会抛出开头那条 husky - pre-commit hook exited with code 1。
这条报错适合谁看?被它拦在提交门外、急着交付又不知道从哪下手的开发同学都适合。我按自己的排障习惯把问题拆成了五类高频原因,照着查基本就能定位。
1.2 报错里其实藏着好几条线索
很多同学一看到 exited with code 1 就慌,其实这个提示只说了一半的信息。真实的排查线索往往藏在它上面的几行输出里。
举个典型的完整报错:
bash复制git commit -m "feat: add login page"
✔ Preparing lint-staged...
✔ Running tasks for staged files...
✔ Applying modifications...
✖ Some of your tasks use `git commit` args (e.g. `--no-verify`)
which will be ignored. Run without args to fix this.
husky - pre-commit hook exited with code 1 (error)
注意看,报错的前半段是 lint-staged 的输出,后半段才是 husky 的退出信息。如果只看最后一行,你会觉得毫无头绪;但把完整输出拉出来,问题往往已经写在里面了。
还有一种更隐蔽的情况:husky 的报错信息里只有一行 error: cannot run .husky/pre-commit: No such file or directory,这通常是文件权限或者 hooksPath 配置错了。所以我的习惯是,遇到这类问题先别急着搜报错原文,先把完整日志存一份,再往下走排查流程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高频原因拆解:为什么你的 commit 会被拦下来
2.1 第一大类:代码检查真的没过
最常见的场景就是 ESLint 或 Prettier 检查不通过。很多团队会在 pre-commit 阶段跑 eslint --fix,但 --fix 只能处理那些能自动修复的规则,遇到不能自动修的(比如变量命名规范、禁止使用 any、复杂度超限),就只能人工改。
你可能会问:我本地明明跑得好好的,怎么提交的时候就不行了?这里有一个非常常见的认知盲区:本地编辑器可能带了各种插件,帮你自动修复了大部分问题,所以你在编辑器里觉得“没事了”,但 commit 时执行的是命令行环境里的全新检查,它不带任何编辑器状态,严格按规则来。
再有一种情况:项目大了之后,lint-staged 默认可能只检查暂存区里改过的文件,但如果配置里写了全量检查(比如 "*.js": "eslint" 而没有加 --cache),就可能把一批历史遗留问题全翻出来。所以看到这个报错时,先确认是不是真的有一批文件被检查出错误了。
2.2 第二大类:lint-staged 在处理暂存文件时出错
lint-staged 的逻辑是:从 git 的暂存区里筛选出符合 glob 匹配模式的文件,然后把它们交给配置的命令执行。这个过程比想象中容易出问题。
最常见的一个坑是:你改了文件,也执行了 git add,但因为某种原因暂存区里的文件列表和你预期的不一样。比如你在某个子目录下执行了 git add .,但目录层级搞错了,实际上只 add 了部分文件。然后 pre-commit 里的 lint 脚本是全量或针对特定目录跑的,这时就会出现“明明我改了文件,但 lint-staged 报告说没有匹配的文件”或者“跑了但检查的是旧版本”的诡异情况。
还有一种更隐蔽的:lint-staged 执行过程中需要修改文件并重新进行暂存,如果某个文件恰好被改动过且没有重新 git add,它就会中断。此时报错里通常能看到 Cannot read properties of undefined 或者提示某个文件处于 unstaged 状态。
所以当你看到 lint-staged 相关的报错,第一步是确认暂存区状态,第三步才是去看匹配规则。命令很简单:
bash复制git status
git diff --cached --name-only
2.3 第三大类:环境与依赖问题
这个类别的报错最迷惑人,因为表面上看起来和代码质量没关系。
先说 Node 版本问题。很多团队里成员用的 Node 版本不统一,有人用 16,有人用 18,还有人可能装了 nvm 但没切对版本。pre-commit 脚本里如果用了某个依赖 Node 高版本特性的包,在低版本下可能直接报语法错误。这时候报错往往不是规则检查不通过,而是类似 SyntaxError: Unexpected token '?' 这种。
再一个是 node_modules 没装全或安装不完整。常见于团队里有人拉完代码直接用 npm install 装到一半就取消了,或者之前用的包管理器不一致(有人用 npm、有人用 pnpm、有人用 yarn),导致依赖目录结构和 husky 期望的不一致。husky 在安装时会写一些脚本到 .husky/ 下,如果依赖缺失,执行时就只能干瞪眼。
还有一类很典型的:core.hooksPath 被某些工具改掉了。husky 的核心机制是把 git 的 hooksPath 设置为 .husky,如果你之前用过别的钩子管理工具,或者手动执行过类似的配置命令,.git/config 里的配置可能被覆盖。检查方法:
bash复制git config core.hooksPath
如果输出不是 .husky,那恭喜你,问题基本找到了。
2.4 第四大类:跨平台和 shell 差异
这是 Windows 用户的高频重灾区。
husky 生成的 pre-commit 脚本本质是一个 shell 脚本,默认用 sh 执行。Windows 环境下,如果你的 git 是官方 Git for Windows,通常会自带 Git Bash,但终端里可能默认走的是 PowerShell 或 cmd。如果你的脚本里写了 #!/usr/bin/env sh,在 Windows 上有时候解析路径会出问题。
更常见的是脚本内容里用了 bash 特有的语法,比如 export FOO=bar、函数定义、${var:-default} 这类写法,在没有 bash 的 shell 环境下就会直接语法报错。
另外一个非常经典的坑是行尾符。如果项目之前有人在 Windows 上把 .husky/pre-commit 文件的换行符改成了 CRLF,git 在不同 core.autocrlf 配置下可能把脚本内容里的 CR 字符也带进来。shell 解析时遇到 \r 就会报 $'\r': command not found。这算是我踩过印象最深的一个坑了,后面会单独说。
2.5 第五大类:暂存区内容本身有问题
这类问题比较容易被忽略。有时候你的 pre-commit 脚本里跑了 lint-staged,但 lint-staged 执行时会修改文件(比如自动修复格式),修改后的文件需要重新 add 到暂存区。如果 lint-staged 因为某种原因没法重新 add,或者你的命令顺序有问题(比如在 lint-staged 之后又加了自己的检查命令),就可能导致后续命令读到的还是旧内容,最终退出码异常。
另外还有加密软件、云盘同步、IDE 的自动保存插件在背后偷偷改了文件状态,这些都可能让暂存区内容和磁盘内容不一致,进而在 hook 执行时产生不可预期的结果。
所以排查这一类问题时,最有效的动作是先把工作区清理干净:git add -A 把改动全部重新暂存,然后再 commit 一次,看问题是否复现。
3. 实战排查与修复:一步步来
3.1 第一步:复现并收集完整的运行日志
看到 exited with code 1 之后,第一件事不是去搜索引擎抄答案,而是先把你完整提交路径上的所有输出复制下来。注意“完整”两个字,要包含之前被刷过的所有内容。
如果刚才的报错输出太短,或者终端被清空了,可以直接在命令行手动执行 hook 内容。比如你的 .husky/pre-commit 里写的是 npm run lint,那就直接跑:
bash复制npm run lint
这样错误信息会非常直观地展示出来。如果里面写的是 npx lint-staged,你就直接跑:
bash复制npx lint-staged
手动执行的好处是绕过了 git 这层壳,能直接看到 hook 脚本本身的输出。这一步至少能帮你区分两种完全不同的情况:
- 脚本内容本身的执行报错(比如语法错误、依赖缺失、环境变量找不到)
- 脚本执行成功但检查不通过(比如 lint 报了具体错误)
这两种情况的处理路径完全不同,前者要去查脚本和依赖,后者要去看具体规则。
3.2 第二步:临时绕过 hook,确认问题边界
如果你急着提交,可以先绕过 hook:
bash复制git commit -m "xxx" --no-verify
但我强烈建议,这个命令只用来应急解锁,不要成为团队习惯。因为它的作用是跳过 pre-commit 和 commit-msg 等钩子,相当于对代码质量和提交规范做了一次“免检”。长期依赖它,pre-commit 就形同虚设了。
更合理的用法是:用它来确认“到底是不是 hook 的问题”。如果你绕过 hook 之后提交成功,说明 git 本身和暂存区没问题,问题出在 hook 脚本或脚本所依赖的命令上。如果绕过之后提交还是失败,那就可能是 git 配置、文件锁或者仓库状态的问题,跟 husky 反而关系不大了。
3.3 第三步:针对具体原因逐个修复
假设你已经通过上面两步定位到了具体原因,下面按类别给出修复方案。
如果你发现是 lint 检查不通过,那就老老实实改代码,或者执行自动修复:
bash复制npx eslint . --fix
npx prettier --write .
改完之后重新 git add,再提交一次。
如果你发现是 lint-staged 报错,先检查暂存区是否正常:
bash复制git status
如果暂存区没有预期文件,重新 add。如果 lint-staged 本身配置有问题,检查 package.json 里的 lint-staged 配置节,比如:
json复制{
"lint-staged": {
"*.{js,ts,vue}": ["eslint --fix", "prettier --write"]
}
}
把范围收窄,并且在本地手动执行 npx lint-staged 验证一下是否通过。
如果你发现是 Node 版本问题,切换版本后重新装依赖:
bash复制nvm use 18
rm -rf node_modules
npm install
如果你发现是 husky 本身没配好,先检查 hooksPath:
bash复制git config core.hooksPath
如果不是 .husky,设置为:
bash复制git config core.hooksPath .husky
同时检查 .husky/pre-commit 文件是否有执行权限。在 Linux/macOS 上用:
bash复制ls -l .husky/pre-commit
没有可执行权限的话加上:
bash复制chmod +x .husky/pre-commit
如果是 Windows 且怀疑是行尾符问题,可以用 git 重新检出文件:
bash复制git add .husky/pre-commit
git rm --cached .husky/pre-commit
git add .husky/pre-commit
或者直接把核心脚本改成不依赖 shell 特性的写法。我见过很多团队为了省事,把 .husky/pre-commit 里的内容简化成一行:
bash复制npx lint-staged
这样在跨平台环境下出问题的概率会小很多。
如果发现是依赖问题,重新安装 husky 并初始化:
bash复制npm install husky@latest --save-dev
npx husky init
新版 husky 的初始化脚本会自动生成 .husky/pre-commit,并在 package.json 里加上 "prepare": "husky",这样后面任何成员 npm install 时都能自动激活 husky。
3.4 第四步:重新触发 commit 验证
修复完之后,重新提交验证:
bash复制git add .
git commit -m "fix: resolve pre-commit hook issue"
正常通过时,你会看到类似这样的输出:
bash复制✔ Preparing lint-staged...
✔ Running tasks for staged files...
✔ Applying modifications...
✔ Cleaning up temporary files...
[main 3f9ba7c] fix: resolve pre-commit hook issue
3 files changed, 45 insertions(+), 12 deletions(-)
如果没有 exited with code 1,说明问题已经解决。
我个人的习惯是,修复之后重启一下终端里的 shell 环境,然后在同一个仓库里连续 commit 两次,确认同样的问题不会再次复现。特别是跨平台类问题,有时候你以为修好了,实际只是这次提交刚好没命中触发条件。
4. 这些坑我替你踩过了:常见问题速查与避坑心得
4.1 高频问题速查表
| 报错特征 | 大概率原因 | 优先排查方向 |
|---|---|---|
SyntaxError: Unexpected token |
Node 版本过低 | node -v 对比团队统一版本 |
$'\r': command not found |
pre-commit 文件被改成 CRLF | 检查行尾符,重新检出 |
cannot run .husky/pre-commit |
缺少执行权限 | chmod +x .husky/pre-commit |
No staged files found |
暂存区为空或 lint-staged 匹配不到 | git status 检查暂存区 |
task not found / command not found |
依赖未安装或 PATH 异常 | npm install,检查脚本内命令 |
| 输出里有大量 ESLint 错误 | 代码检查不通过 | 手动执行 npm run lint 修复 |
| 输出全部正常但退出码还是 1 | 最后的命令返回了非 0 | 检查 hook 最后一行命令 |
| 配置了但 hook 总是跳过 | core.hooksPath 配置错误 | git config core.hooksPath |
这张表不是万能的,但覆盖了我这些年遇到的大多数场景。如果你的报错不在表里,请记住一条铁律:看完整日志,不要只看最后一行。
4.2 几条值得长期坚持的使用习惯
第一条,别把 --no-verify 当免死金牌。它确实能救急,但如果你的团队里有人习惯了跳过检查,那些被漏掉的问题就会在 CI、生产环境或者队友的机器上爆出来。损失的时间远大于提交前改一个问题的时间。
第二条,husky 的初始化脚本和配置一定要纳入版本管理。.husky 目录要提交到仓库里,package.json 里的 prepare 脚本也要保留。这样新人拉代码后只用 npm install,husky 就自动就位,不会出现“我明明配了 hook 但队友那边不起作用”的尴尬。
第三条,提交前多做一个动作:先 git status 看一眼暂存区。很多时候 hook 报错只是因为你漏 add 了一个文件,或者 add 了一个你不小心改动到一半的文件。这 10 秒钟能省下之后十分钟的排查时间。
第四条,如果团队成员多、操作系统杂,我建议在 .husky/pre-commit 里只保留最关键的检查命令,把逻辑复杂、依赖特定 shell 特性的部分下沉到 npm scripts 或者 Node 脚本里。脚本越简单,跨平台踩坑的概率就越低。
4.3 几个值得记住的冷知识
关于 husky 的版本差异,这里多说一句。新版 husky(v9 之后)已经不再使用 husky.sh 那个引导脚本了,生成的 pre-commit 文件非常简单。如果你看到网上老教程里要你手动创建 .huskyrc、或者在 package.json 里配置 "husky": { "hooks": { "pre-commit": "..." } },那基本都是旧版写法,新版已经不支持了,可以直接忽略。
另外,exited with code 1 里的 1 只是一个约定俗成的“失败”标识,具体含义要看脚本自己。有些脚本会把不同错误场景映射到不同的退出码,比如 1 表示普通失败、2 表示用法错误。但在 husky 这里,你只需要知道非 0 就代表没通过。
还有一点容易忽略:如果在 submodule 或 worktree 里使用 git,husky 的 hooksPath 可能会发生相对路径解析错误。这种情况比较少见,但一旦遇到,优先检查 git rev-parse --git-path hooks 的解析结果。
5. 关于这个错误,我后来是怎么做的
这个报错本身不复杂,但它背后折射出来的问题很典型:git 钩子机制很强大,但团队的代码规范和开发环境往往在真正提交的那一刻才会露出马脚。
说一个真实体验。我之前在一个混合团队里带过一阵子前端组,Windows 和 macOS 的成员各占一半。一开始 pre-commit 脚本里跑的是 npm run lint && npm run test,结果 Windows 同事三天两头报 exited with code 1,macOS 这边却一切正常。排查到最后发现,问题根本不是代码,而是路径分隔符加 shell 语法兼容性。后来我们把 pre-commit 简化成 npx lint-staged,把复杂的检查逻辑拆到独立的 npm script 里,同类报错几乎消失了。
所以如果你也被这个错误卡住了,我的建议顺序是:先看完整日志,再对照速查表定位,修复后重新提交验证。如果时间允许,顺手把团队里的 Node 版本和 husky 初始化方式统一起来,这类问题会从根本上减少很多。
最后分享一个小技巧:如果某个版本的 hook 脚本确实没问题,但你觉得它的输出太啰嗦,可以在脚本末尾加一句 echo "pre-commit check passed" 作为可视化确认。这样以后看到正常的提交输出,心里会踏实很多。
