拓冰建站拓冰建站
首页 / 资讯中心 / 正文

husky pre-commit报错排查:从exited with code 1到根治

很多前端开发者在提交代码时都遇到过这个让人头皮发麻的瞬间git commit按下去终端直接飘红一行husky - pre-commit hook exited with code 1把提交彻底拦住了。我第一次碰到时也愣了一下第一反应是“我没改啥啊”第二反应是搜“怎么跳过 hook”。后来踩坑踩多了才明白这个报错的核心从来不是“怎么绕过”而是“为什么被拦”。这篇文章我会把 husky 在 pre-commit 阶段做了什么、报错背后的真实原因、以及从现象到根因的完整排查链路都拆开讲清楚。不管你是刚开始接触 husky 的初级前端还是已经在团队里维护 Git 规范的开发按着这套思路走基本能在一两分钟内定位问题。这个报错出现的地方非常固定——通常是项目里配置了 husky lint-staged 作为提交前的自动化检查工具。它的存在本身是好事说明团队在试图用工具兜底代码质量。但工具报错时的信息确实不够直观exited with code 1这种措辞对新手来说等于啥也没说。我见过太多人在这个报错面前选择了最粗暴的解决方案git commit --no-verify。这招当然有效但用多了 hook 就形同虚设。所以这篇的重点不是教你逃生而是带你真正看懂这行报错以及学会一套通用的排查方法。1. 提交被 pre-commit 拦住时屏幕上的输出到底在说什么1.1 pre-commit 不是玄学它在 Git 工作流中的真实位置要理解这个报错先得搞清楚 husky 在这条链路上扮演的角色。Git 本身就有一套 hooks 机制在特定时机比如 commit 之前、push 之前去执行.git/hooks/目录下的脚本。如果脚本执行成功退出码为 0流程继续如果失败退出码非 0Git 中断操作。这是一套非常朴素的机制朴素到很多时候团队里每个人都得手动复制一份脚本才能生效。husky 做的事就是把“手动复制脚本”这件事自动化。你只要在项目中装了 husky它会把 hook 脚本统一放到.husky/目录下然后通过core.hooksPath配置让 Git 去那个目录找脚本。比如项目里经常看到的.husky/pre-commit文件里面可能就一行npx lint-staged。当你在终端执行git commit时Git 会在正式创建提交之前触发 pre-commit 钩子husky 负责把这个钩子引导起来然后执行里面配置的命令。所以husky - pre-commit hook exited with code 1这句话翻译成人话就是提交之前的自动化检查脚本跑到一半失败了退出码是 1Git 出于安全策略拒绝生成这次提交。1.2 退出码的真正含义它只是结论不是原因这里有个很容易被忽视的点exited with code 1只是一个“结论”说明某个进程以失败状态退出了。在 Unix/Linux 体系中退出码 0 代表成功非 0 都代表失败而 1 是最常见的一般性错误码。关键在于那个失败进程是谁是 husky 吗不一定。husky 只是整个链条的“总调度”真正干活的是你在.husky/pre-commit里配置的那条命令——绝大多数项目里是lint-staged或者直接一段npm run lint、npm test。命令执行失败后退出码会一级一级往上传递lint-staged 返回 1husky 接收到了于是 git 看到的也是 1。所以当你在屏幕上看到exited with code 1时真正的错误信息早就在它上面输出了。我说句不太好听的实话大多数人之所以觉得这个报错难解决不是因为它真的难而是根本没往上翻终端盯着最后一行红色就开始搜解决方案了。1.3 先别急你的终端里可能已经写好了答案这里给你一个具体的观察方法下次再看到这个报错先别复制错误去搜索先把终端往上翻。你大概率能看到两种典型输出之一。第一种是 lint-staged 的输出以 ESLint 检查为例它会列出具体报错的文件名、行号、列号、错误规则和描述比如✖ eslint --fix found some errors. Please fix them and try committing again. error: foo is assigned a value but never used no-unused-vars这种情况下答案已经写在脸上项目里有代码没通过 lint 检查改掉就好。第二种输出非常短可能只是sh: lint-staged: command not found这类。这说明根本不是代码问题而是执行环境出了问题命令本身找不到。看到这里你应该明白了本项目里遇到exited with code 1你要做的是顺着终端输出往上找到“真正的错误行”而不是对着 husky 那一行干瞪眼。下一节我就把这些常见错误的根本原因分类拆开。2. 元凶列表从代码规范到 hook 脚本自身的隐藏问题2.1 ESLint / Prettier 检查不通过大多数情况的真正原因如果你是第一次遇到这个报错那么约 90% 的情况是 lint 检查没通过。为什么这么说因为 lint-staged 这个工具的设计目标就是在提交前只对暂存区的文件跑一遍检查而绝大多数 pre-commit 配置的第一项就是代码检查。看一个很典型的配置在package.json里{ lint-staged: { *.{js,jsx,ts,tsx}: [eslint --fix, prettier --write] } }这段配置的意思是暂存区里凡是匹配到.js、.jsx、.ts、.tsx的文件提交前先执行eslint --fix自动修复再执行prettier --write统一格式。问题在于--fix只能修掉一部分规则比如自动补分号、修缩进遇到no-unused-vars变量声明了没用、react-hooks/rules-of-hooksHook 调用规则这类需要人工判断的错误ESLint 会直接返回失败状态。还有一点容易踩坑如果 ESLint 报的是 error 级别的错误lint-staged 会中断如果只是 warning 级别的提示默认情况下不会阻断提交但有的团队会把某个规则配成 error或者直接在 ESLint 配置里--max-warnings0那 warning 也会变成阻断项。碰到后者别奇怪这是团队故意为之的。2.2 lint-staged 本身配置出错匹配规则和命令参数第二种常见元凶是 lint-staged 的配置问题。很多人第一次配的时候会把 glob 匹配规则写错或者命令参数不对导致 lint-staged 在执行时直接抛错。举几个我实际见过的问题第一个问题配置了不存在的文件模式。比如你只改了src/index.ts但 lint-staged 配置的匹配规则是*.js那么这个文件根本不会被匹配到lint-staged 会认为没有任务可跑。注意lint-staged 在“没有匹配到任何文件”时默认是静默通过的这不算报错。真正会报错的是规则写得太宽把不该检查的文件也卷进来了。第二个问题在命令里错误使用占位符。lint-staged 支持用{}作为文件列表的占位符但如果你配置成{ *.js: eslint --fix {} }在某些 shell 环境下文件数量过多时命令会超长或者传参方式不对导致 ESLint 解析失败。更稳妥的写法是省略{}让 lint-staged 自动把匹配到的文件追加到命令末尾。第三个问题tsconfig 的 project 引用错误。如果项目用了 TypeScript 的parserOptions.project即类型感知的 lint 规则lint-staged 默认只把暂存的文件列表传给 ESLint而不是整个项目的文件集合这会导致 ESLint 报“无法找到 project”之类的错误。解决办法是在 eslintrc 里对 lint-staged 暂存文件场景做特殊处理或者直接用eslint . --ext .ts这种全量检查。2.3 .husky/pre-commit 文件本身shebang、权限和执行路径从 husky 6 开始hook 脚本变成了项目仓库内的真实文件好处是跟着版本库走团队每个人行为一致坏处是这个文件本身也会出问题。最常见的情况是文件没有可执行权限。这种情况在 Linux、macOS 下会直接报Permission denied之类的错误Windows 上则可能表现为 Git Bash 里无法执行。排查方法很简单进入项目目录执行ls -l .husky/pre-commit正常的话开头应该有一串包含x的权限位比如-rwxr-xr-x。如果看不到x就补一句chmod x .husky/pre-commit还有一种情况是 shebang 行丢了或者写错。hook 文件本质上是个 shell 脚本第一行必须声明用什么解释器比如#!/bin/sh或#!/usr/bin/env sh。如果缺失Git 在调用时会用默认 shell 执行偶尔会出现奇怪的兼容性问题。另外有一个很容易被忽略的场景如果你的.husky/pre-commit是通过某个脚手架生成的生成之后的文件可能是空文件或者内容被截断了。执行时它静默通过看起来好像没报错但 hook 实际没干活。这时打开文件看一眼通常就能发现问题。2.4 依赖安装不完整node_modules 缺失或版本错位还有一种非常“隐蔽”但十分常见的情况本地依赖没装全。比如你新拉了一个分支、切了一个新项目直接改了代码就想提交。但是npm install之后项目里压根没有 lint-staged或者 husky 本身就没能正常初始化。这种场景下报错通常是sh: 1: lint-staged: not found husky - pre-commit hook exited with code 1你盯着屏幕看半天也看不出“代码错了”因为确实不是代码问题。问题在依赖树损坏或缺失。解决方式是重装依赖npm install # 或者更彻底的 rm -rf node_modules package-lock.json npm install另外从 husky 6 开始安装 husky 后需要手动执行一次初始化命令npx husky init或者npm run prepare取决于项目里 scripts 怎么配的。如果初始化没跑.husky/目录下的 hook 文件就不会注册到 Git 的 hooksPath 中提交时 hook 可能根本不会触发——这又是一种“没报错但 hook 失效”的情况比报错更麻烦。版本错位也值得一提。如果你从 husky 4 直接升级到 husky 7老的.huskyrc配置文件是不再被识别的。升级之后必须重新按文件方式配置 hook 脚本否则你会陷入“明明配置了但就是不生效”的尴尬。为了便于快速对照我把上面这些常见原因和对应的排查动作整理成一张表现象可能原因快速定位方式输出里有 ESLint 报错的行和规则代码不符合 lint 规则按报错信息修代码lint-staged: not found依赖没装或 node_modules 损坏npm install重装Permission deniedhook 文件没有执行权限chmod x .husky/pre-commit终端输出很短只有code 1可能是命令整体失败或 hook 脚本损坏打开.husky/pre-commit检查内容提交时 hook 根本没触发.husky目录未初始化或 hooksPath 被改查git config core.hooksPath3. 一次从头到尾的真实排查复现、定位、根治三步走3.1 第一步把屏幕上的报错“摊开来看”前面说了那么多理论这里我完整走一遍真实场景的排查过程。假设你执行git commit -m feat: add login page终端输出是这样的✔ Preparing lint-staged... ✔ Running tasks for staged files... ✖ lint-staged failed due to a git error. husky - pre-commit hook exited with code 1第一眼看上去似乎很简单但其实中间状态已经丢了一部分信息。我通常的习惯是再多跑一次但这次把输出重定向到文件里避免终端刷新太快看不清git commit -m feat: add login page 21 | tee /tmp/commit.log然后打开/tmp/commit.log一帧一帧看。大多数时候你会看到中间某一行写着具体的报错比如某个 Node 命令抛出的异常堆栈或者 ESLint 的规则列表。如果日志里什么具体信息都没有那就进入第二步。3.2 第二步绕过 Git直接复现 hook 脚本本身这是一个我强烈推荐的技巧别让 git 当“中间商”直接在终端里手动执行 hook 脚本。比如项目里.husky/pre-commit的内容通常是#!/usr/bin/env sh . $(dirname -- $0)/_/husky.sh npx lint-staged那你就可以直接在项目根目录执行sh .husky/pre-commit这样等价于把 pre-commit 阶段实际运行的脚本拉出来裸跑一遍。好处是输出更直接而且不受 git 的某些包装干扰。如果这步复现了同样的非 0 退出再加一层调试参数继续跑sh -x .husky/pre-commit-x参数会让 shell 把每一步执行的命令都打印出来看到底卡在哪一行。这一步在 hook 脚本逻辑复杂时尤其管用。如果是 lint-staged 本身报错还可以单独调它npx lint-staged --debug它会把每一步匹配文件、执行命令的过程都打印出来包括暂存区有哪些文件、为每个文件分配了什么任务。很多时候你一看就知道是配置问题还是代码问题。3.3 第三步查看完整日志和调试信息如果裸跑 hook 也没暴露问题那就要考虑是不是 husky 自身的状态异常了。先查一下 Git 当前认的 hooks 目录在哪里git config --get core.hooksPath正常情况下husky 7 会输出.husky。如果输出为空或者指向了别的路径说明 husky 的初始化状态被破坏了。比如你可能之前手动配过别的 Git 工具把core.hooksPath改走了也可能 husky 的prepare脚本被 npm 跳过压根没注册。接下来检查.husky/_/目录是否存在。这是 husky 内部存放辅助脚本的地方如果它不存在说明初始化不完整。修复方式npm install husky --save-dev npx husky init还有一种少见但真实存在的情况你在终端里用了一个被其他包管理器污染的环境变量。比如 PATH 里没有包含项目的node_modules/.bin导致 hook 里的命令找不到。husky 生成的脚本通常会主动把node_modules/.bin塞进 PATH但如果你的 shell 配置太特殊这一步可能被覆盖。3.4 真正修复从“绕过”到“根治”定位到根因之后修复动作本身通常很简单。我用一个自己经历的真实案例收尾这节当时同事的电脑上每次提交都报同样的错日志里只显示lint-staged failed但没有任何 ESLint 的报错。我裸跑sh .husky/pre-commit后发现是turbo命令抛了个“找不到 turbo”的错误。再看package.json同事用的是 pnpm 安装的依赖但node_modules/.bin/turbo因为 pnpm 的软链结构没有暴露出来。解决方案是在项目根目录加一个.npmrc配置node-linkerhoisted然后重新pnpm install问题解决。这类问题不动手敲一遍光靠搜索引擎是不容易定位到的。所以我的建议一直是遇到exited with code 1先手动复现再下结论。4. 容易被甩锅给“环境”的坑Windows、换行符与 Node 工具链4.1 core.hooksPath 被改写husky“失联”的经典场景前面提到过core.hooksPath这个配置它是 husky 生效的基石。但有个容易被忽略的场景某些全局工具或脚手架会在安装时擅自修改这个配置。比如有些 monorepo 工具、或者某些 Git 增强工具会把 hooksPath 指到它们自己的目录下然后 husky 就“失联”了。失联的表现有两种一种是 hook 完全不执行提交一路畅通——这种情况很多人反而会觉得“诶最近提交好顺”其实团队的检查规范已经静默失效了另一种是 hook 执行了但执行的是另一个同名文件行为不符合预期产生各种奇怪的报错。所以每当你怀疑 husky 行为异常第一件事不是改代码而是确认git config --get core.hooksPath如果结果和预期不符可以手动改回git config core.hooksPath .husky4.2 Windows 用户的 CRLF 与 Git Bash 路径转换提到 Git 在 Windows 上的问题换行符是绕不开的坑。如果你在 Windows 上开发Git 默认可能会把 checkout 出来的文件转成 CRLF回车换行而 hook 脚本本质是 shell 脚本Linux 系的 shell 只认 LF换行。一个非常典型的现象是hook 执行时报$\r: command not found。看起来莫名其妙但其实是脚本里每一行的末尾多了一个\r字符shell 把\r当成命令的一部分去解析自然找不到。这种问题的根治方案是在项目根目录加一个.gitattributes文件强制对 hook 脚本使用 LF.husky/** text eollf同时建议在整个项目里统一换行符策略* textauto然后把仓库里的文件重新规范化一次git add --renormalize .这样一来团队成员不管在 Windows 还是 macOS 上 clonecheckout 出来的.husky/文件都会是 LF 结尾。4.3 Node 版本与包管理器差异带来的工具链怪问题还有一种常见的“环境玄学”来自 Node 版本。如果你用 nvm 或其他版本管理工具在不同项目间切换 Node 版本是很常见的。有些 lint 规则依赖原生模块比如某些编译器优化的依赖切换 Node 版本后node_modules里的二进制可能不匹配执行时直接报错。这种现象尤其容易出现在husky本身安装受 Node 版本影响的场景。如果你的项目是用 Node 16 安装的依赖但当前 shell 用的是 Node 20某些二进制兼容性可能导致工具无法启动。最直接的解决方法是重新构建依赖npm rebuild # 或者干脆删除重装 rm -rf node_modules npm install另外不同包管理器的行为差异也值得注意。pnpm 用户可能会遇到node_modules/.bin里的命令无法正常调用的问题因为 pnpm 的依赖组织结构是符号链接式的。husky 7 官方对 pnpm 有兼容方案但如果你在 monorepo 里跑了多个子项目每个子项目的 node_modules 布局可能不一样hook 脚本里如果用了绝对路径或相对路径很容易踩坑。稳妥的做法是hook 脚本里尽量使用当前包管理器能够解析的本地命令而不是全局命令或者干脆用npx让它按流程去查找。我还见过有人因为npm_config_user_agent环境变量的干扰导致 hook 里的命令走到了错误版本的 node_modules。这类问题通常没有标准答案唯一的经验法则是报错输出里如果出现了奇怪的路径先去看node_modules/.bin/下对应的命令到底指向哪、能不能直接执行。5. 长期顺滑运行的几个习惯从 hook 设计到团队规范5.1 把 hook 当 CI 看待保持轻量、聚焦暂存区我见过很多团队其实是被自己的 hook 折磨的。原因很简单他们把太多任务塞进了 pre-commit比如全量跑单元测试、全量跑 lint、全量跑 TypeScript 类型检查。项目小的时候还好项目一大了每次提交都要等几十秒甚至几分钟大家自然就开始用--no-verify逃票。正确的思路是把 pre-commit 当成本地 CI 的“轻量检查”只做两件事——修复能自动修复的格式问题prettier / eslint --fix以及检查暂存区文件的硬伤。全量测试、全量类型检查交给 CI 跑而不是让它在每次提交前拖慢节奏。lint-staged 的意义正在于此它只处理暂存区的文件保证检查范围最小、速度最快。这也是我推荐所有项目优先选择 lint-staged而不是在 hook 里写npm run lint的原因。如果一个项目连 lint-staged 都会慢到难以忍受那大概率是 lint 配置本身有性能问题需要优化规则或者关闭一些耗时的类型感知规则。5.2 几个让我少踩坑的小习惯最后分享几个我长期实践下来觉得值得保持的习惯。第一新 clone 项目后先npm install再手动执行一次sh .husky/pre-commit确认 hook 本身能正常运行。这一步花不了几秒钟但能提前暴露换行符、依赖、权限等诸多问题省得第一次提交时手忙脚乱。第二hook 脚本里尽量别用全局命令。有人喜欢写lint-staged或者eslint这些都是依赖 PATH 解析的一旦环境不匹配就挂。更稳的写法是直接用npx lint-staged让本地的 node_modules 去决定调用哪个版本。第三遇到 hook 自身的问题可以临时用git commit --no-verify先把工作保存下来但一定要给自己立个规矩当天必须修复不能留过夜。这个开关用多了团队的质量防线就会慢慢失守。第四团队协作时建议在.gitattributes里固定换行符并且把 husky、lint-staged 的版本写死避免不同同事之间因为依赖版本不一致产生诡异差异。老项目尤其要注意升级 husky 是大版本变更别在普通需求里顺手升级应该单独立个任务处理。我自己刚开始接触 husky 时也走过弯路遇到exited with code 1第一反应就是--no-verify跑路直到有一次线上出现了一个本可以在提交前拦截的 bug我才开始认真对待这个报错。后来养成了“先摊开看、再手动复现、最后根治”的习惯反而发现这个报错几乎很少让我卡住十分钟以上。说到底它不是一个艰难的谜题只是需要你多一点耐心把终端里的输出从头看到尾。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门