Husky:把原生 Git Hook 接入 Node 仓库
Husky 做的事很窄:把仓库中的 .husky/ 脚本接到 Git 的 Hook 查找链。它不选择暂存文件,不提供 Lint 规则,也不让客户端 Hook 变成不可绕过控制。这个边界反而是它的优势——Node 仓库可以继续用现有 package scripts,只让 Husky 负责“在正确的 Git 时点调用哪个稳定入口”。
本文以 Husky 9.1.7 为实验基线,要求 Node >=18。包版本、包管理器生命周期、core.hooksPath 来源和 Hook 文件权限共同决定是否执行。
初始化时观察 Git 配置变化
在有锁文件的仓库中固定开发依赖,再初始化:
npm install --save-dev --save-exact husky@9.1.7
npx husky init
npx husky --version || npm ls husky
git config --show-origin --get core.hooksPath
git status --shorthusky init 创建 .husky/pre-commit,并在 package.json 中加入 prepare。prepare 使后续依赖安装能够重新建立 Hook 接入,但它受包管理器、安装参数和生产依赖策略影响。团队应把初始化放进文档化的开发环境入口,并在新 clone 中实际验证。
一个最小配置只调用仓库脚本:
{
"scripts": {
"prepare": "husky",
"check:staged": "lint-staged",
"check:quality": "eslint . && prettier --check ."
},
"devDependencies": {
"husky": "9.1.7"
}
}# .husky/pre-commit
npm run check:stagedHook 文件越薄越容易排障。参数、并发、文件选择与规则放回各自工具配置,不要把几十行 shell 复制进 .husky/pre-commit。脚本非零退出时 Git 会中止当前提交,退出码必须原样传播,不能在末尾无条件 exit 0。
core.hooksPath 是第一现场
Git 默认从 $GIT_DIR/hooks 查找 Hook,Husky 通过 core.hooksPath 把查找位置切到管理目录。遇到“.husky/ 明明存在却没运行”,先看 Git 实际读取的值和来源:
git config --show-origin --get core.hooksPath
git rev-parse --git-path hooks
git rev-parse --show-toplevel
npm run prepare用户级或其他工具也可能设置 core.hooksPath。不能看到一个异常值就直接 unset;先确认作用域、配置文件来源和所有者。一个 Git 仓库只有一条实际 Hook 查找链,多套 Hook 管理器争抢它时,最后写入者会覆盖前一套。
用失败样例证明新 clone 已启用
提交 .husky/ 只保存脚本,不会把 .git 内状态带给其他 clone。验收需要一个临时仓库或测试分支:先让 Hook 明确返回非零,尝试提交并确认 HEAD 未前进,再恢复真实脚本。
# 临时故障注入,只用于可丢弃实验
printf '#!/bin/sh\nexit 23\n' > .husky/pre-commit
git add .husky/pre-commit
set +e
git commit -m "test: hook must block"
hook_rc=$?
set -e
test "$hook_rc" -ne 0实验前保存原脚本,结束后恢复并再次运行项目检查。不要在共享分支留下 exit 23,也不要用真实未提交工作测试恢复能力。Windows 还要确认 Git 实际使用的 shell、行尾和可执行行为;同一 Hook 在 PowerShell 终端可用,不代表 GUI 客户端拥有相同 PATH。
GUI 与版本管理器的环境不同
从桌面 GUI 启动的 Git 往往没有交互式 shell 初始化,因此找不到 nvm、fnm、Volta 或自定义 Node 路径。第一证据是 Hook 内的 PATH、command -v node、node --version 和当前目录,而不是反复重装 Husky。
Husky 支持用户初始化脚本用于统一环境,但项目不能依赖每个人手工维护一份未知内容。更稳妥的方式是由团队开发镜像或统一 Node 管理器保证稳定路径,并在 CLI、IDE 和常用 GUI 上各跑一次失败样例。Hook 日志不要打印 registry Token、代理口令或完整环境变量。
生产安装与 CI 要显式处理生命周期
只安装生产依赖的镜像通常不需要 Husky。若 prepare 在没有 devDependencies 的阶段执行,可能导致构建失败。生产构建可在明确的安装阶段设置 HUSKY=0,但不能把这个变量注入开发机和质量 CI:
HUSKY=0 npm ci --omit=dev质量 CI 不依赖 Husky 模拟本地提交。它在干净 checkout 中直接运行 npm ci 与 npm run check:quality,并由分支保护要求通过。Husky 缩短开发者反馈,服务端检查才提供不可绕过边界。
Monorepo 只安装一个 Hook 入口
一个 Git 仓库通常在根目录安装 Husky。各 package 可以有自己的检查脚本或 lint-staged 配置,但不应分别运行 husky init 修改同一个 core.hooksPath。根 Hook 根据仓库级脚本分发任务,依赖工作区工具决定受影响包。
从子目录运行初始化时要确认仓库根和 package 根的关系。Hook 的工作目录、包管理器 workspace 行为和相对路径必须有样例覆盖。根目录文件、不同 package、重命名和删除文件都要验证,避免只对最常改的前端包生效。
原生 Git Hook 语义仍然有效
Husky 没有改变 Git 的 Hook 时序。pre-commit 在生成 commit 前运行,commit-msg 可以检查提交消息,非零退出中止当前动作。Git 的 --no-verify 能绕过支持该选项的客户端 Hook,环境变量 HUSKY=0 也会禁用 Husky。
因此 Hook 不应承担唯一的 Secret、安全或发布控制。受控绕过需要记录原因、规则、批准、补检与到期时间,CI 仍执行对应检查。机器人账号不应因为自动化身份就永久设置 HUSKY=0 并免除服务端门禁。
仓库代码会以开发者权限执行
拉取不受信任仓库后,不应自动运行未知的依赖安装和 Hook 初始化。.husky/、package scripts、Husky 包与传递依赖都进入代码评审和依赖锁。Hook 原则上不需要生产 Token、发布密钥或云写权限;必须访问私有服务时只给短期只读凭证,并隔离 Fork 与外部贡献路径。
Hook 耗时应记录 P50/P95、失败类型和绕过原因,不记录源码内容。持续缓慢的任务应缩小输入或移到 CI,而不是无限提高超时。Husky 只负责调用,性能瓶颈通常在被调用工具或工作区解析中。
故障与恢复
Hook 未运行先看 core.hooksPath、prepare 是否执行、文件是否存在和脚本能否在 Git 使用的 shell 中启动。Hook 运行但找不到命令,比较 CLI 与 GUI 的 PATH。生产镜像安装失败,检查生命周期是否在省略 devDependencies 后仍调用 Husky。多个工具交替失效,查谁最后改写了 Hook 路径。
任务失败后保留 git status --short、工作区 diff 与暂存 diff。Husky 自身不会替 lint-staged 管理部分暂存恢复,也不应在通用故障脚本里执行 git reset --hard。先修复被调用命令,再重试 Hook,避免通过禁用 Husky掩盖规则错误。
安全卸载
退出 Husky 时先从 prepare 删除安装调用,删除 .husky/,卸载依赖,再检查 Hook 路径。只有确认配置确由当前仓库 Husky 管理时才执行:
git config --show-origin --get core.hooksPath
git config --unset core.hooksPath
npm uninstall husky回滚升级则恢复上一版 package.json、lockfile、Hook 脚本与初始化行为,并在新 clone 上重新跑失败样例。卸载完成不等于治理完成;替代 Hook 或 CI 检查必须已经生效,否则只是让本地门禁静默消失。
